threat_intelligence1532 wordsRead on Arc Codex

A polymorphic phishing page (that occasionally breaks itself), (Thu, Aug 27th)

A polymorphic phishing page (that occasionally breaks itself) As I’ve mentioned before in some of my diaries, from time to time, I like to go over phishing messages that get caught in my various spam traps or sent to us here at the Internet Storm Center. After looking at enough phishing messages, one quickly gets used to seeing the same lures, the same credential-harvesting pages and, quite often, the same obfuscation techniques over and over again. But even something that seems to be “run-of-the-mill” at first glance can sometimes turn out to be quite interesting. One such message was recently sent to our handler inbox, and as you can see, there was very little about it that would indicate that it would be worth a deeper look. The link in the message pointed to a URL with the following, quite usual, structure: hxxps[:]//addresses[.]performs[.]vu/communications.html?good=[recipient_address] Nevertheless, what happened after the link was opened was somewhat less usual. Instead of displaying a phishing page, the browser remained effectively stuck for about 30 seconds, while utilization of one CPU core in the virtual machine I was using quickly rose to 100 %. Since retrieving the HTML source itself was almost instantaneous, it seemed clear that the delay wasn't caused by the server, and instead something in the page itself was preventing the browser from finishing its work. Although a quick look at the source code showed that almost all of the page consisted of heavily obfuscated JavaScript, the reason for the unusual behavior fortunately wasn't too difficult to identify. Among other things, the script contained two functions, which are slightly reformatted here for easier readability: function _il(m) { for(k=0; 64>k; k++) { m[_lV(_ie(),k)]=k } return m } function _YF(m,h) { var v=""; for(k=m; k<=h; k++) { v=v+String.fromCharCode(k) } return v } As you can see, both functions use k as a counter in their for loops. The first function is part of a decoding routine, and its loop counter is expected to go from 0 to 63. The second function is a helper used by the same routine to construct strings from ranges of character codes – it is used (among other places) in the _ie() function, which is called by the first function. The problem is that k isn't declared locally in either one of these functions. This becomes important because _ie(), which is called during every iteration of the first loop, uses _YF() several times to construct the Base64 alphabet. Its final call is _YF(47,47), which produces the ‘/’ character (ASCII code 47). Since the counter k used by _YF() is global, this final call also changes the value of k used by the outer loop. _YF(47,47) first sets k to 47, executes its loop once and then increments k to 48. At that point, the condition k <= 47 is no longer true, so _YF() returns with the global value of k left at 48. Control then returns to the outer for loop, whose own increment changes k from 48 to 49. Since 49 is still smaller than 64, another iteration starts and _ie() is called again. Its final _YF(47,47) call once more leaves k at 48. The outer loop therefore never progresses beyond 49. The resulting sequence therefore looks roughly like this: 48 -> 49 48 -> 49 48 -> 49 ... This explained both why the page never rendered and why the browser was keeping one CPU core rather busy. Changing the inner routine to use its own local counter was sufficient to let the decoding process finish. After removing the remaining layers of obfuscation, what emerged was an otherwise completely unremarkable credential-stealing page. At this point, the most likely explanation seemed fairly straightforward – the authors of the page had simply shot themselves in the foot by using a broken obfuscation mechanism. Nevertheless, this proved not to be the case, since when I accessed the original URL again a little later, the page loaded normally. Another attempt to load the page was also successful, as were several subsequent ones. More interestingly, while all of the resulting pages ultimately displayed the same credential-stealing form, their source code wasn't the same. Function and variable names differed across page loads, functions appeared in a different order, numerical constants were expressed using different arithmetic operations and a large encoded block of code, which contained the actual payload with the form, changed as well. Even the innocuous-looking page title varied between requests using words like "Solution", "Viewer", "Credentials", "Private" and "Authenticate". It therefore appeared that the first response wasn't a permanently broken copy of the phishing page at all. Rather, the server seemed to generate polymorphic variants of the page and I had simply happened to receive a “broken” one when I first accessed the target URL. To test this hypothesis, I used a simple script to retrieve the same URL 50 times and, with some help from an LLM, compared the resulting samples. Among the 50 samples (which all had different SHA-256 hashes), there were 21 different page titles, and, more importantly, 49 deobfuscated successfully while one became stuck in an endless loop – just like the first page I had the luck to land on. The reason was effectively identical to what happened in the first page I encountered. In this variant, the two relevant functions had different randomized names, but both of their loops had once again been assigned the same undeclared variable k. The inner loop therefore repeatedly reset the value used by the outer one and prevented the decoder from completing. Once this collision was corrected, the sample decoded normally as well. The polymorphism wasn't limited to the initial JavaScript wrapper. The 50 page variants (if we include the one I had to manually “fix”) produced 50 different versions of the final phishing HTML. Form and input names, CSS classes, element identifiers and parameters used when loading images were changed, as was the placement of zero-width characters inside visible strings, which were used as a further obfuscation/anti-analysis mechanism. In spite of all these changes, however, the page presented to the user and its basic functionality remained essentially identical. Polymorphic phishing pages are, of course, not new. The concept has been discussed for well over a decade in academic circles[1], and phishing pages which generate random HTML attribute values for individual visits have been used in the wild for years[2]. It has also previously been shown that JavaScript lends itself quite well to producing multiple versions of source code which look different while performing the same task[3] (which is the basis for the simplest implementation of polymorphism at the code level). The rationale behind such an approach is fairly obvious – hashes, randomly generated identifiers and many simple string-based signatures become significantly less useful if every request produces what is basically a completely new copy of a malicious page. Although polymorphism certainly shouldn't be thought of as some universal mechanism for bypassing security controls, as the underlying logic and behavior of the pages remains the same, and many structural characteristics inevitably survive most transformations, it does raise the cost of detection mechanisms which rely too heavily on static artifacts... Though, in this case, it apparently also raised the cost for the threat actor, since at least some victims would end up with a non-functioning page (at least on a first load), given that of the approximately 56 samples I collected (50 using the script + my original manual attempts), two pages were broken. Although it would be unreasonable to draw any firm conclusions about the actual failure rate of the mechanisms used, it is clear that the original endless loop wasn't just a “one-off” corrupted response and that whatever generates the code can repeatedly create non-functional pages. Which brings us to one final question – what was actually generating the code? Given the current popularity of generative AI, it is tempting to consider an LLM-based backend. This isn't entirely far-fetched either – in January, Unit 42 demonstrated a proof-of-concept in which an LLM was used to generate syntactically different phishing JavaScript in real time, resulting in a unique variant for individual visits[4]. There is, however, nothing in the samples which would prove that an LLM is involved here, and a conventional polymorphic obfuscator seems to be a much more plausible explanation, given that the transformations between individual page copies are quite systematic, and the recurring failure caused by reused global variable names would fit quite nicely with a relatively simple random renaming and reordering mechanism which doesn't properly account for variable scope. In any case, had the first page loaded normally, I would almost certainly have dismissed it as yet another run-of-the-mill phishing site. As it turned out, though, the obfuscation mechanism intended to make the page more difficult to detect was also capable of making it somewhat ineffective at stealing credentials... which made the sample considerably more interesting than it initially appeared. And – to end on a positive note – the sample did also provide a good lesson to any aspiring programmers out there – never use undeclared global variables as your loop counters. [1] https://link.springer.com/chapter/10.1007/978-3-642-02617-1_28 [2] https://www.zscaler.com/blogs/security-research/evolution-phishing-kits [3] https://www.akamai.com/blog/security/the-tale-of-double-javascript-obfuscated-scam [4] https://unit42.paloaltonetworks.com/real-time-malicious-javascript-through-llms/ ----------- Jan Kopriva LinkedIn Nettles Consulting Comments

How it works

Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.

Questions are cached — you'll always get the same 5 for this article.